chromium/chrome/test/data/webui/chromeos/settings/os_languages_page/os_languages_page_v2_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 {LanguageHelper, LanguagesBrowserProxyImpl, LanguagesMetricsProxyImpl, LanguagesPageInteraction, LanguageState, LifetimeBrowserProxyImpl, OsSettingsChangeDeviceLanguageDialogElement, OsSettingsLanguagesPageV2Element, SettingsLanguagesElement} from 'chrome://os-settings/lazy_load.js';
import {CrActionMenuElement, CrCheckboxElement, CrLinkRowElement, CrPolicyIndicatorElement, CrSettingsPrefs, Router, routes, settingMojom, SettingsPrefsElement} from 'chrome://os-settings/os_settings.js';
import {assert} from 'chrome://resources/js/assert.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {getDeepActiveElement} from 'chrome://resources/js/util.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, assertLT, assertNull, assertStringContains, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {FakeSettingsPrivate} from 'chrome://webui-test/fake_settings_private.js';
import {fakeDataBind, waitAfterNextRender} from 'chrome://webui-test/polymer_test_util.js';
import {isVisible} from 'chrome://webui-test/test_util.js';

import {FakeLanguageSettingsPrivate, getFakeLanguagePrefs} from '../fake_language_settings_private.js';
import {TestLifetimeBrowserProxy} from '../test_os_lifetime_browser_proxy.js';

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

suite('<os-settings-languages-page-v2>', () => {
  let languageHelper: LanguageHelper;
  let languagesPage: OsSettingsLanguagesPageV2Element;
  let languagesList: HTMLElement;
  let actionMenu: CrActionMenuElement;
  let browserProxy: TestLanguagesBrowserProxy;
  let lifetimeProxy: TestLifetimeBrowserProxy;
  let metricsProxy: TestLanguagesMetricsProxy;
  let languageSettingsPrivate: FakeLanguageSettingsPrivate;
  let settingsLanguages: SettingsLanguagesElement;
  let settingsPrefs: SettingsPrefsElement;

  // Initial value of enabled languages pref used in tests.
  const INITIAL_LANGUAGES = 'en-US,sw';

  suiteSetup(() => {
    CrSettingsPrefs.deferInitialization = true;

    // Sets up test browser proxy.
    browserProxy = new TestLanguagesBrowserProxy();
    LanguagesBrowserProxyImpl.setInstanceForTesting(browserProxy);

    lifetimeProxy = new TestLifetimeBrowserProxy();
    LifetimeBrowserProxyImpl.setInstance(lifetimeProxy);

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

  setup(async () => {
    assert(window.trustedTypes);
    document.body.innerHTML =
        window.trustedTypes.emptyHTML as unknown as string;

    settingsPrefs = document.createElement('settings-prefs');
    const settingsPrivate = new FakeSettingsPrivate(getFakeLanguagePrefs());
    settingsPrefs.initialize(settingsPrivate);
    document.body.appendChild(settingsPrefs);
    await CrSettingsPrefs.initialized;

    // Sets up fake languageSettingsPrivate API.
    languageSettingsPrivate = browserProxy.getLanguageSettingsPrivate() as
        unknown as FakeLanguageSettingsPrivate;
    languageSettingsPrivate.setSettingsPrefsForTesting(settingsPrefs);

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

    // Creates page with data bindings for prefs and data model.
    languagesPage = document.createElement('os-settings-languages-page-v2');
    languagesPage.prefs = settingsPrefs.prefs;
    fakeDataBind(settingsPrefs, languagesPage, 'prefs');
    languagesPage.languages = settingsLanguages.languages;
    fakeDataBind(settingsLanguages, languagesPage, 'languages');
    languagesPage.languageHelper = settingsLanguages.languageHelper;
    fakeDataBind(settingsLanguages, languagesPage, 'language-helper');
    document.body.appendChild(languagesPage);

    const element =
        languagesPage.shadowRoot!.querySelector<HTMLElement>('#languagesList');
    assertTrue(!!element);
    languagesList = element;
    actionMenu = languagesPage.$.menu.get();

    languageHelper = languagesPage.languageHelper;
    await languageHelper.whenReady();
  });

  teardown(() => {
    languagesPage.remove();
    settingsLanguages.remove();
    settingsPrefs.remove();

    browserProxy.reset();
    lifetimeProxy.reset();
    metricsProxy.reset();
    languageSettingsPrivate.reset();

    Router.getInstance().resetRouteForTesting();
  });

  suite('language menu', () => {
    /*
     * Finds, asserts and returns the menu item for the given i18n key.
     * @param i18nKey Name of the i18n string for the item's text.
     * @return Menu item.
     */
    function getMenuItem(i18nKey: string): HTMLButtonElement|CrCheckboxElement {
      const i18nString = loadTimeData.getString(i18nKey);
      assertTrue(!!i18nString);
      const menuItems =
          actionMenu.querySelectorAll<HTMLButtonElement|CrCheckboxElement>(
              '.dropdown-item');
      const menuItem = Array.from(menuItems).find(
          (item: HTMLButtonElement|CrCheckboxElement) =>
              item.textContent?.trim() === i18nString);
      assertTrue(!!menuItem, `Menu item "${i18nKey}" not found`);
      return menuItem;
    }

    /*
     * Checks the visibility of each expected menu item button.
     * param {!Object<boolean>} Dictionary from i18n keys to expected
     *     visibility of those menu items.
     */
    function assertMenuItemButtonsVisible(
        buttonVisibility: {[key: string]: boolean}): void {
      assertTrue(actionMenu.open);
      for (const buttonKey of Object.keys(buttonVisibility)) {
        const buttonItem = getMenuItem(buttonKey);
        assertEquals(
            !buttonVisibility[buttonKey], buttonItem.hidden,
            `Menu item "${buttonKey}" hidden`);
      }
    }

    test('removes language when starting with 3 languages', () => {
      // Enables a language which we can then disable.
      languageHelper.enableLanguage('no');

      // Populates the dom-repeat.
      flush();

      // Finds the new language item.
      const items = languagesList.querySelectorAll<HTMLElement>('.list-item');
      const domRepeat = languagesList.querySelector('dom-repeat');
      assertTrue(!!domRepeat);
      const item = Array.from(items).find(
          (el) => domRepeat.itemForElement(el) &&
              (domRepeat.itemForElement(el).language.code === 'no'));
      assertTrue(!!item);

      // Opens the menu and selects Remove.
      const button = item.querySelector('cr-icon-button');
      assertTrue(!!button);
      button.click();

      assertTrue(actionMenu.open);
      const removeMenuItem = getMenuItem('removeLanguage');
      assertFalse(removeMenuItem.disabled);
      assertFalse(removeMenuItem.hidden);
      removeMenuItem.click();
      assertFalse(actionMenu.open);

      assertEquals(
          INITIAL_LANGUAGES,
          languagesPage.getPref('intl.accept_languages').value);
    });

    test('removes language when starting with 2 languages', () => {
      const items = languagesList.querySelectorAll<HTMLElement>('.list-item');
      const domRepeat = languagesList.querySelector('dom-repeat');
      assertTrue(!!domRepeat);
      const item = Array.from(items).find(
          (el) => domRepeat.itemForElement(el) &&
              domRepeat.itemForElement(el).language.code === 'sw');
      assertTrue(!!item);

      // Opens the menu and selects Remove.
      const button = item.querySelector('cr-icon-button');
      assertTrue(!!button);
      button.click();

      assertTrue(actionMenu.open);
      const removeMenuItem = getMenuItem('removeLanguage');
      assertFalse(removeMenuItem.disabled);
      assertFalse(removeMenuItem.hidden);
      removeMenuItem.click();
      assertFalse(actionMenu.open);

      assertEquals(
          'en-US', languagesPage.getPref('intl.accept_languages').value);
    });

    test('the only translate blocked language is not removable', () => {
      //'en-US' is preconfigured to be the only translate blocked language.
      assertDeepEquals(
          ['en-US'], languagesPage.prefs.translate_blocked_languages.value);
      const items = languagesList.querySelectorAll<HTMLElement>('.list-item');
      const domRepeat = languagesList.querySelector('dom-repeat');
      assertTrue(!!domRepeat);
      const item = Array.from(items).find(
          (el) => domRepeat.itemForElement(el) &&
              domRepeat.itemForElement(el).language.code === 'en-US');
      assertTrue(!!item);

      // Opens the menu and selects Remove.
      const button = item.querySelector('cr-icon-button');
      assertTrue(!!button);
      button.click();

      assertTrue(actionMenu.open);
      const removeMenuItem = getMenuItem('removeLanguage');
      assertTrue(removeMenuItem.disabled);
      assertFalse(removeMenuItem.hidden);
    });

    test('device language is removable', () => {
      // 'en-US' is the preconfigured UI language.
      assertEquals(
          'en-US', languagesPage.get('languages.prospectiveUILanguage'));
      // Add 'sw' to translate_blocked_languages.
      languagesPage.setPrefValue(
          'translate_blocked_languages', ['en-US', 'sw']);
      flush();

      const items = languagesList.querySelectorAll<HTMLElement>('.list-item');
      const domRepeat = languagesList.querySelector('dom-repeat');
      assertTrue(!!domRepeat);
      const item = Array.from(items).find(
          (el) => domRepeat.itemForElement(el) &&
              domRepeat.itemForElement(el).language.code === 'en-US');
      assertTrue(!!item);

      // Opens the menu and selects Remove.
      const button = item.querySelector('cr-icon-button');
      assertTrue(!!button);
      button.click();

      assertTrue(actionMenu.open);
      const removeMenuItem = getMenuItem('removeLanguage');
      assertFalse(removeMenuItem.disabled);
      assertFalse(removeMenuItem.hidden);
      removeMenuItem.click();
      assertFalse(actionMenu.open);

      assertEquals('sw', languagesPage.getPref('intl.accept_languages').value);
    });

    test('single preferred language is not removable', () => {
      languagesPage.setPrefValue('intl.accept_languages', 'sw');
      languagesPage.setPrefValue('settings.language.preferred_languages', 'sw');
      flush();
      const items = languagesList.querySelectorAll<HTMLElement>('.list-item');
      const domRepeat = languagesList.querySelector('dom-repeat');
      assertTrue(!!domRepeat);
      const item = Array.from(items).find(
          (el) => domRepeat.itemForElement(el) &&
              domRepeat.itemForElement(el).language.code === 'sw');
      assertTrue(!!item);

      // Opens the menu and selects Remove.
      const button = item.querySelector('cr-icon-button');
      assertTrue(!!button);
      button.click();

      assertTrue(actionMenu.open);
      const removeMenuItem = getMenuItem('removeLanguage');
      assertTrue(removeMenuItem.disabled);
      assertFalse(removeMenuItem.hidden);
    });

    test('removing a language does not remove related input methods', () => {
      const sw = '_comp_ime_abcdefghijklmnopqrstuvwxyzabcdefxkb:sw:sw';
      const swUS = 'ime_abcdefghijklmnopqrstuvwxyzabcdefxkb:us:sw';
      languageHelper.addInputMethod(sw);
      languageHelper.addInputMethod(swUS);
      assertEquals(
          4, languagesPage.get('languages.inputMethods.enabled').length);

      // Disable Swahili. The Swahili-only keyboard should not be removed.
      languageHelper.disableLanguage('sw');
      assertEquals(
          4, languagesPage.get('languages.inputMethods.enabled').length);
    });

    test('has move up/down buttons', () => {
      // Adds several languages.
      for (const language of ['en-CA', 'en-US', 'tk', 'no']) {
        languageHelper.enableLanguage(language);
      }

      flush();

      const menuButtons = languagesList.querySelectorAll<HTMLButtonElement>(
          '.list-item cr-icon-button.icon-more-vert');

      // First language should not have "Move up" or "Move to top".
      menuButtons[0]!.click();
      assertMenuItemButtonsVisible({
        moveToTop: false,
        moveUp: false,
        moveDown: true,
      });
      actionMenu.close();

      // Second language should not have "Move up".
      menuButtons[1]!.click();
      assertMenuItemButtonsVisible({
        moveToTop: true,
        moveUp: false,
        moveDown: true,
      });
      actionMenu.close();

      // Middle languages should have all buttons.
      menuButtons[2]!.click();
      assertMenuItemButtonsVisible({
        moveToTop: true,
        moveUp: true,
        moveDown: true,
      });
      actionMenu.close();

      // Last language should not have "Move down".
      menuButtons[menuButtons.length - 1]!.click();
      assertMenuItemButtonsVisible({
        moveToTop: true,
        moveUp: true,
        moveDown: false,
      });
      actionMenu.close();
    });

    test('test translate target language is labelled', () => {
      const targetLanguageCode = languagesPage.get('languages.translateTarget');
      assertTrue(!!targetLanguageCode);

      // Add 'en' to have more than one translate-target language.
      languageHelper.enableLanguage('en');
      const isTranslateTarget = (languageState: LanguageState) =>
          languageHelper.convertLanguageCodeForTranslate(
              languageState.language.code) === targetLanguageCode;
      const translateTargets =
          languagesPage.get('languages.enabled').filter(isTranslateTarget);
      assertGT(translateTargets.length, 1);
      // Ensure there is at least one non-translate-target language.
      assertLT(
          translateTargets.length,
          languagesPage.get('languages.enabled').length);

      const listItems =
          languagesList.querySelectorAll<HTMLElement>('.list-item');
      const domRepeat = languagesList.querySelector('dom-repeat');
      assertTrue(!!domRepeat);

      let translateTargetLabel;
      let item;
      let numVisibles = 0;
      Array.from(listItems).forEach(el => {
        item = domRepeat.itemForElement(el);
        if (item) {
          translateTargetLabel = el.querySelector('.target-info');
          assertTrue(!!translateTargetLabel);
          if (getComputedStyle(translateTargetLabel).display !== 'none') {
            numVisibles++;
            assertEquals(
                targetLanguageCode,
                languageHelper.convertLanguageCodeForTranslate(
                    item.language.code));
          }
        }
        assertEquals(
            1, numVisibles,
            'Not exactly one target info label (' + numVisibles + ').');
      });
    });

    test('toggle translate checkbox for a language', async () => {
      // Open options for 'sw'.
      const languageOptionsDropdownTrigger =
          languagesList.querySelectorAll('cr-icon-button')[1];
      assertTrue(!!languageOptionsDropdownTrigger);
      languageOptionsDropdownTrigger.click();
      assertTrue(actionMenu.open);

      // 'sw' supports translate to the target language ('en').
      const translateOption =
          getMenuItem('offerToTranslateThisLanguage') as CrCheckboxElement;
      assertFalse(translateOption.disabled);
      assertTrue(translateOption.checked);

      // Toggle the translate option.
      translateOption.click();
      assertFalse(
          await metricsProxy.whenCalled('recordTranslateCheckboxChanged'));
      assertDeepEquals(
          ['en-US', 'sw'],
          languagesPage.prefs.translate_blocked_languages.value);

      // Menu should stay open briefly.
      assertTrue(actionMenu.open);

      // Menu closes after delay
      const kMenuCloseDelay = 100;
      await new Promise(r => setTimeout(r, kMenuCloseDelay + 1));
      assertFalse(actionMenu.open);
    });

    test('translate checkbox disabled for translate blocked language', () => {
      // Open options for 'en-US'.
      const languageOptionsDropdownTrigger =
          languagesList.querySelectorAll('cr-icon-button')[0];
      assertTrue(!!languageOptionsDropdownTrigger);
      languageOptionsDropdownTrigger.click();
      assertTrue(actionMenu.open);

      // 'en-US' does not support checkbox.
      const translateOption = getMenuItem('offerToTranslateThisLanguage');
      assertTrue(translateOption.disabled);
    });

    test('disable translate hides language-specific option', () => {
      // Disables translate.
      languagesPage.setPrefValue('translate.enabled', false);

      // Open options for 'sw'.
      const languageOptionsDropdownTrigger =
          languagesList.querySelectorAll('cr-icon-button')[1];
      assertTrue(!!languageOptionsDropdownTrigger);
      languageOptionsDropdownTrigger.click();
      assertTrue(actionMenu.open);

      // The language-specific translation option should be hidden.
      const translateOption =
          actionMenu.querySelector<HTMLElement>('#offerTranslations');
      assertTrue(!!translateOption);
      assertTrue(translateOption.hidden);
    });

    test('Deep link to add language', async () => {
      const params = new URLSearchParams();
      params.append('settingId', settingMojom.Setting.kAddLanguage.toString());
      Router.getInstance().navigateTo(routes.OS_LANGUAGES_LANGUAGES, params);

      flush();

      const deepLinkElement =
          languagesPage.shadowRoot!.querySelector<HTMLElement>('#addLanguages');
      assertTrue(!!deepLinkElement);
      await waitAfterNextRender(deepLinkElement);
      assertEquals(
          deepLinkElement, getDeepActiveElement(),
          'Add language button should be focused for settingId=1200.');
    });
  });

  suite('change device language dialog', () => {
    let dialog: OsSettingsChangeDeviceLanguageDialogElement;
    let dialogItems: NodeListOf<HTMLElement>;
    let cancelButton: HTMLButtonElement;
    let actionButton: HTMLButtonElement;

    /**
     * Returns the list items in the dialog.
     */
    function getListItems(): Element[] {
      // 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.
      const dialogEl = dialog.$.dialog;
      const list = dialogEl.querySelector('iron-list');
      if (list && list.hidden) {
        return [];
      }
      return [...dialogEl.querySelectorAll('.list-item:not([hidden])')];
    }

    setup(() => {
      assertNull(languagesPage.shadowRoot!.querySelector(
          'os-settings-change-device-language-dialog'));
      const crButton =
          languagesPage.shadowRoot!.querySelector<HTMLButtonElement>(
              '#changeDeviceLanguage');
      assertTrue(!!crButton);
      crButton.click();
      flush();

      const element = languagesPage.shadowRoot!.querySelector(
          'os-settings-change-device-language-dialog');
      assertTrue(!!element);
      dialog = element;

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

      // The fixed-height dialog's iron-list should stamp far fewer than
      // 50 items.
      dialogItems =
          dialog.$.dialog.querySelectorAll('.list-item:not([hidden])');
      assertGT(dialogItems.length, 1);
      assertLT(dialogItems.length, 50);

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

    teardown(() => {
      languagesPage.remove();
      dialog.remove();
    });

    test('has action button working correctly', () => {
      // selecting a language enables action button
      dialogItems[0]!.click();
      assertFalse(actionButton.disabled);

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

    test('setting device language restarts device', async () => {
      // selects a language
      dialogItems[0]!.click();  // en-CA
      assertFalse(actionButton.disabled);

      actionButton.click();
      assertEquals(
          'en-CA', await browserProxy.whenCalled('setProspectiveUiLanguage'));
      assertEquals(
          LanguagesPageInteraction.RESTART,
          await metricsProxy.whenCalled('recordInteraction'));
      await lifetimeProxy.whenCalled('signOutAndRestart');
    });

    test(
        'setting device language adds it to front of enabled language if not present',
        async () => {
          languagesPage.setPrefValue('intl.accept_languages', 'en-US,sw');
          languagesPage.setPrefValue(
              'settings.language.preferred_languages', 'en-US,sw');
          // selects a language
          dialogItems[0]!.click();  // en-CA
          assertFalse(actionButton.disabled);

          actionButton.click();
          assertEquals(
              'en-CA',
              await browserProxy.whenCalled('setProspectiveUiLanguage'));
          assertTrue(languagesPage.getPref('intl.accept_languages')
                         .value.startsWith('en-CA'));
        });

    test(
        'setting device language moves already enabled language to front',
        async () => {
          languagesPage.setPrefValue('intl.accept_languages', 'en-US,sw,en-CA');
          languagesPage.setPrefValue(
              'settings.language.preferred_languages', 'en-US,sw,en-CA');
          flush();

          // selects a language
          dialogItems[0]!.click();  // en-CA
          assertFalse(actionButton.disabled);

          actionButton.click();
          assertEquals(
              'en-CA',
              await browserProxy.whenCalled('setProspectiveUiLanguage'));
          assertTrue(languagesPage.getPref('intl.accept_languages')
                         .value.startsWith('en-CA'));
        });

    // Test that searching languages works whether the displayed or native
    // language name is queried.
    test('searches languages', () => {
      const searchInput = dialog.shadowRoot!.querySelector('cr-search-field');
      assertTrue(!!searchInput);

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

      // Issue query that matches the |displayedName| in lowercase.
      searchInput.setValue('greek');
      flush();
      assertEquals(1, getListItems().length);
      assertStringContains(getListItems()[0]!.textContent!, 'Greek');

      // Issue query that matches the |nativeDisplayedName|.
      searchInput.setValue('Ελληνικά');
      flush();
      assertEquals(1, getListItems().length);

      // Issue query that does not match any language.
      searchInput.setValue('egaugnal');
      flush();
      assertEquals(0, getListItems().length);
    });

    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);
    });

    test('languages are sorted on native display name', () => {
      // See https://crbug.com/1184064 for more details.
      // We can't test whether the order is *deterministic* w.r.t. device
      // language, as changing device language is not possible in a test, so we
      // do the next best thing and check if it's sorted on native display name.

      function getNativeDisplayName(text: string): string {
        return text.includes(' - ') ? text.split(' - ')[0]! : text;
      }

      const items = getListItems();
      const nativeDisplayNames =
          items.map(item => getNativeDisplayName(item.textContent!.trim()));

      const sortedNativeDisplayNames =
          [...nativeDisplayNames].sort((a, b) => a.localeCompare(b, 'en'));
      assertDeepEquals(nativeDisplayNames, sortedNativeDisplayNames);
    });
  });

  suite('records metrics', () => {
    test('when adding languages', async () => {
      const button = languagesPage.shadowRoot!.querySelector<HTMLButtonElement>(
          '#addLanguages');
      assertTrue(!!button);
      button.click();
      flush();
      await metricsProxy.whenCalled('recordAddLanguages');
    });

    test('when disabling translate.enable toggle', async () => {
      languagesPage.setPrefValue('translate.enabled', true);
      const toggle = languagesPage.shadowRoot!.querySelector<HTMLButtonElement>(
          '#offerTranslation');
      assertTrue(!!toggle);
      toggle.click();
      flush();

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

    test('when enabling translate.enable toggle', async () => {
      languagesPage.setPrefValue('translate.enabled', false);
      const toggle = languagesPage.shadowRoot!.querySelector<HTMLButtonElement>(
          '#offerTranslation');
      assertTrue(!!toggle);
      toggle.click();
      flush();

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

    test('when clicking on Manage Google Account language', async () => {
      // The below would normally create a new window using `window.open`, which
      // would change the focus from this test to the new window.
      // Prevent this from happening by overriding `window.open`.

      window.open = () => {
        return null;
      };

      const button = languagesPage.shadowRoot!.querySelector<HTMLButtonElement>(
          '#manageGoogleAccountLanguage');
      assertTrue(!!button);
      button.click();
      flush();
      assertEquals(
          LanguagesPageInteraction.OPEN_MANAGE_GOOGLE_ACCOUNT_LANGUAGE,
          await metricsProxy.whenCalled('recordInteraction'));
    });

    test('when clicking on "learn more" about web languages U2', async () => {
      const link =
          languagesPage.shadowRoot!.querySelector('#webLanguagesDescription');
      assertTrue(!!link);
      const anchor = link.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_WEB_LANGUAGES_LEARN_MORE,
          await metricsProxy.whenCalled('recordInteraction'));
    });
  });
});

suite('change device language button', () => {
  let page: OsSettingsLanguagesPageV2Element;

  function createPage(): void {
    page = document.createElement('os-settings-languages-page-v2');
    document.body.appendChild(page);
    flush();
  }

  setup(() => {
    assert(window.trustedTypes);
    document.body.innerHTML =
        window.trustedTypes.emptyHTML as unknown as string;
  });

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

  test('is hidden for guest users', () => {
    loadTimeData.overrideValues({
      isGuest: true,
    });
    createPage();

    assertNull(page.shadowRoot!.querySelector('#changeDeviceLanguage'));
    assertNull(
        page.shadowRoot!.querySelector('#changeDeviceLanguagePolicyIndicator'));
  });

  test('is disabled for secondary users', () => {
    loadTimeData.overrideValues(
        {isGuest: false, isSecondaryUser: true, primaryUserEmail: 'test.com'});
    createPage();

    const changeDeviceLanguageButton =
        page.shadowRoot!.querySelector<HTMLButtonElement>(
            '#changeDeviceLanguage');
    assertTrue(!!changeDeviceLanguageButton);
    assertTrue(changeDeviceLanguageButton.disabled);
    assertFalse(changeDeviceLanguageButton.hidden);

    const changeDeviceLanguagePolicyIndicator =
        page.shadowRoot!.querySelector<CrPolicyIndicatorElement>(
            '#changeDeviceLanguagePolicyIndicator');
    assertTrue(!!changeDeviceLanguagePolicyIndicator);
    assertFalse(changeDeviceLanguagePolicyIndicator.hidden);
    assertEquals(
        'test.com', changeDeviceLanguagePolicyIndicator.indicatorSourceName);
  });

  test('is enabled for primary users', () => {
    loadTimeData.overrideValues({
      isGuest: false,
      isSecondaryUser: false,
    });
    createPage();

    const changeDeviceLanguageButton =
        page.shadowRoot!.querySelector<HTMLButtonElement>(
            '#changeDeviceLanguage');
    assertTrue(!!changeDeviceLanguageButton);
    assertFalse(changeDeviceLanguageButton.disabled);
    assertFalse(changeDeviceLanguageButton.hidden);

    assertNull(
        page.shadowRoot!.querySelector('#changeDeviceLanguagePolicyIndicator'));
  });

  suite('app languages settings', () => {
    let page: OsSettingsLanguagesPageV2Element;

    function createPage(): void {
      page = document.createElement('os-settings-languages-page-v2');
      document.body.appendChild(page);
      flush();
    }

    setup(() => {
      assert(window.trustedTypes);
      document.body.innerHTML =
          window.trustedTypes.emptyHTML as unknown as string;
    });

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

    test('Enable perAppLanguage flag, show app languages section', () => {
      loadTimeData.overrideValues({
        isPerAppLanguageEnabled: true,
      });
      createPage();
      const appLanguagesSection =
          page.shadowRoot!.querySelector<CrLinkRowElement>(
              '#appLanguagesSection');
      assertTrue(
          isVisible(appLanguagesSection),
          '#appLanguagesSection is not visible.');
    });

    test('Disable perAppLanguage flag, hide app languages section', () => {
      loadTimeData.overrideValues({
        isPerAppLanguageEnabled: false,
      });
      createPage();
      const appLanguagesSection =
          page.shadowRoot!.querySelector<CrLinkRowElement>(
              '#appLanguagesSection');
      assertFalse(
          isVisible(appLanguagesSection),
          '#appLanguagesSection is not hidden.');
    });
  });
});