chromium/chrome/test/data/webui/chromeos/settings/common/app_language_selection_dialog/app_language_selection_dialog_test.ts

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

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

import {AppLanguageSelectionDialogElement, AppLanguageSelectionItemElement} from 'chrome://os-settings/lazy_load.js';
import {AppLanguageSelectionDialogEntryPoint, AppManagementStore, CrButtonElement, CrSearchFieldElement, IronListElement} from 'chrome://os-settings/os_settings.js';
import {App, AppType} from 'chrome://resources/cr_components/app_management/app_management.mojom-webui.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {assertEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {fakeMetricsPrivate, MetricsTracker} from 'chrome://webui-test/metrics_test_support.js';
import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
import {isVisible} from 'chrome://webui-test/test_util.js';

import {FakePageHandler} from '../../app_management/fake_page_handler.js';
import {isHidden, setupFakeHandler} from '../../app_management/test_util.js';

type AppConfig = Partial<App>;
// Enum for assertion tags, denoting which item list is under test.
enum ListType {
  SUGGESTED = 'SUGGESTED_LIST',
  FILTERED = 'FILTERED_LIST',
}

suite('<app-language-selection-dialog>', () => {
  const appLanguageSelectionItemTag = 'app-language-selection-item' as const;
  const listItemId = '#listItem';
  const ironIconTag = 'iron-icon' as const;
  const deviceLanguageLabel = 'Device language';
  const lastSetAppLocalePrefKey = 'arc.last_set_app_locale';
  const defaultPref: chrome.settingsPrivate.PrefObject = {
    key: lastSetAppLocalePrefKey,
    type: chrome.settingsPrivate.PrefType.STRING,
    value: '',
  };
  let appLanguageSelectionDialog: AppLanguageSelectionDialogElement;
  let fakeHandler: FakePageHandler;
  let searchField: CrSearchFieldElement;
  let confirmButton: CrButtonElement;
  let metrics: MetricsTracker;

  setup(async () => {
    metrics = fakeMetricsPrivate();
    appLanguageSelectionDialog =
        document.createElement('app-language-selection-dialog');

    fakeHandler = setupFakeHandler();
    await flushTasks();
  });

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

  async function addDialog(
      arcConfig: AppConfig, appId: string,
      lastSetAppLocalePref: chrome.settingsPrivate.PrefObject = defaultPref,
      entryPoint: AppLanguageSelectionDialogEntryPoint =
          AppLanguageSelectionDialogEntryPoint.APPS_MANAGEMENT_PAGE):
      Promise<void> {
    const arcApp: App = await fakeHandler.addApp(appId, arcConfig);
    await fakeHandler.flushPipesForTesting();
    appLanguageSelectionDialog.app = arcApp;
    appLanguageSelectionDialog.entryPoint = entryPoint;
    appLanguageSelectionDialog.prefs = {
      arc: {last_set_app_locale: lastSetAppLocalePref},
    };

    document.body.appendChild(appLanguageSelectionDialog);
    flush();

    assertTrue(
        appLanguageSelectionDialog.$.dialog.open, 'Dialog is not opened');
    const suggestedList = getSuggestedList();
    assertTrue(!!suggestedList, '#suggestedItemsList not found');
    const searchFieldTemp =
        appLanguageSelectionDialog.shadowRoot!
            .querySelector<CrSearchFieldElement>('cr-search-field');
    assertTrue(!!searchFieldTemp);
    searchField = searchFieldTemp;

    const confirmButtonTemp =
        appLanguageSelectionDialog.shadowRoot!.querySelector<CrButtonElement>(
            '.action-button');
    assertTrue(!!confirmButtonTemp);
    confirmButton = confirmButtonTemp;
  }

  function getSuggestedList(): IronListElement {
    return appLanguageSelectionDialog.shadowRoot!
        .querySelector<IronListElement>('#suggestedItemsList')!;
  }

  function getFilteredList(): IronListElement|null {
    const filteredList =
        appLanguageSelectionDialog.shadowRoot!.querySelector<IronListElement>(
            '#filteredItemsList');
    return filteredList;
  }

  function getSuggestedItems(): NodeListOf<AppLanguageSelectionItemElement> {
    return getSuggestedList().querySelectorAll(appLanguageSelectionItemTag);
  }

  function getFilteredItems(): NodeListOf<AppLanguageSelectionItemElement> {
    const filteredList = getFilteredList();
    assertTrue(
        !!filteredList,
        'Filtered list should be present when fetching filtered items.');
    return filteredList.querySelectorAll(appLanguageSelectionItemTag);
  }

  function assertLanguageItem(
      list: NodeListOf<AppLanguageSelectionItemElement>, idx: number,
      textContent: string, isSelected: boolean, listType: ListType): void {
    const tag = `[${listType}-item#${idx}]`;
    assertTrue(
        idx < list.length,
        `${tag} Invalid idx ${idx} is larger than list size ${list.length}`);
    const languageItem = list[idx]!.shadowRoot!.querySelector(listItemId);
    assertTrue(!!languageItem, `${tag} #list-item not found`);
    assertTrue(!!languageItem.textContent, `${tag} Item textContent not found`);
    assertTrue(
        languageItem.textContent.includes(textContent),
        `${tag} Invalid text ${languageItem.textContent}`);
    if (isSelected) {
      assertTrue(
          isVisible(languageItem.querySelector(ironIconTag)),
          `${tag} Selection icon is not visible`);
    } else {
      assertTrue(
          isHidden(languageItem.querySelector(ironIconTag)),
          `${tag} Selection icon is not hidden`);
    }
  }

  function setSearchQuery(query: string): void {
    searchField.setValue(query);
    flush();
  }

  function getNoSearchResultField(): HTMLDivElement|null {
    return appLanguageSelectionDialog.shadowRoot!.querySelector<HTMLDivElement>(
        '#noSearchResults');
  }


  test('No selected locale, device language is pre-selected', async () => {
    const testDisplayName = 'testDisplayName';
    const arcOptions: AppConfig = {
      type: AppType.kArc,
      supportedLocales: [{
        localeTag: 'test1',
        displayName: testDisplayName,
        nativeDisplayName: '',
      }],
    };
    await addDialog(arcOptions, 'no-selected-locale');

    // Suggested item should only contain device language, and selected.
    const suggestedItems = getSuggestedItems();
    assertEquals(1, suggestedItems.length);
    assertLanguageItem(
        suggestedItems, /* idx= */ 0, deviceLanguageLabel,
        /* isSelected= */ true, ListType.SUGGESTED);
    // Filtered item should contain the supported locale, but not selected.
    const filteredItems = getFilteredItems();
    assertEquals(1, filteredItems.length);
    assertLanguageItem(
        filteredItems, /* idx= */ 0, testDisplayName, /* isSelected= */ false,
        ListType.FILTERED);
  });

  test(
      'No selected locale with multiple supported locales, displayName ' +
          'and nativeDisplayName is merged when possible',
      async () => {
        const sameDisplayName = 'sameDisplayName';
        const testDisplayName = 'testDisplayName';
        const testNativeDisplayName = 'testNativeDisplayName';
        const arcOptions: AppConfig = {
          type: AppType.kArc,
          supportedLocales: [
            {
              localeTag: 'test1',
              displayName: sameDisplayName,
              nativeDisplayName: sameDisplayName,
            },
            {
              localeTag: 'test2',
              displayName: testDisplayName,
              nativeDisplayName: testNativeDisplayName,
            },
          ],
        };
        await addDialog(arcOptions, 'no-selected-locale-multiple-locales');

        const filteredItems = getFilteredItems();
        assertEquals(2, filteredItems.length);
        // If displayName and nativeDisplayName is same, only use displayName.
        assertLanguageItem(
            filteredItems, /* idx= */ 0, sameDisplayName,
            /* isSelected= */ false, ListType.FILTERED);
        // Name should be concatenated (displayName - nativeDisplayName).
        assertLanguageItem(
            filteredItems, /* idx= */ 1,
            testDisplayName + ' - ' + testNativeDisplayName,
            /* isSelected= */ false, ListType.FILTERED);
      });

  test(
      'Selected locale with one supported locale, selectedLocale should ' +
          'be pre-selected and filteredItems empty',
      async () => {
        const testDisplayName = 'testDisplayName';
        const arcOptions: AppConfig = {
          type: AppType.kArc,
          supportedLocales: [{
            localeTag: 'test1',
            displayName: testDisplayName,
            nativeDisplayName: '',
          }],
          selectedLocale: {
            localeTag: 'test1',
            displayName: testDisplayName,
            nativeDisplayName: '',
          },
        };
        await addDialog(arcOptions, 'selected-locale-one-supported-locale');

        // Suggested items should contain deviceLanguage and selectedLocale.
        const suggestedItems = getSuggestedItems();
        assertEquals(2, suggestedItems.length);
        // Device language shouldn't be selected.
        assertLanguageItem(
            suggestedItems, /* idx= */ 0, deviceLanguageLabel,
            /* isSelected= */ false, ListType.SUGGESTED);
        // Test item should be selected.
        assertLanguageItem(
            suggestedItems, /* idx= */ 1, testDisplayName,
            /* isSelected= */ true, ListType.SUGGESTED);
        // Filtered list should be hidden.
        assertTrue(
            isHidden(getFilteredList()), '#filteredItemsList is not hidden');
      });

  test(
      'Selected locale with multiple supported locales, filteredItems ' +
          'should not be empty',
      async () => {
        const testDisplayName = 'testDisplayName';
        const testDisplayName2 = 'testDisplayName2';
        const arcOptions: AppConfig = {
          type: AppType.kArc,
          supportedLocales: [
            {
              localeTag: 'test1',
              displayName: testDisplayName,
              nativeDisplayName: '',
            },
            {
              localeTag: 'test2',
              displayName: testDisplayName2,
              nativeDisplayName: '',
            },
          ],
          selectedLocale: {
            localeTag: 'test1',
            displayName: testDisplayName,
            nativeDisplayName: '',
          },
        };
        await addDialog(
            arcOptions, 'selected-locale-multiple-supported-locales');

        const filteredItems = getFilteredItems();
        assertEquals(1, filteredItems.length);
        // Selected item should be in the suggestedList
        assertLanguageItem(
            filteredItems, /* idx= */ 0, testDisplayName2,
            /* isSelected= */ false, ListType.FILTERED);
      });

  test(
      'Toggle to device language, selectedLocale should move to ' +
          'device language',
      async () => {
        const testDisplayName = 'testDisplayName';
        const arcOptions: AppConfig = {
          type: AppType.kArc,
          supportedLocales: [{
            localeTag: 'test1',
            displayName: testDisplayName,
            nativeDisplayName: '',
          }],
          selectedLocale: {
            localeTag: 'test1',
            displayName: testDisplayName,
            nativeDisplayName: '',
          },
        };
        await addDialog(arcOptions, 'toggle-to-device-language');

        const suggestedItems = getSuggestedItems();
        assertEquals(2, suggestedItems.length);
        assertLanguageItem(
            suggestedItems, /* idx= */ 0, deviceLanguageLabel,
            /* isSelected= */ false, ListType.SUGGESTED);
        suggestedItems[0]!.shadowRoot!
            .querySelector<HTMLDivElement>(listItemId)!.click();
        // Device language should be selected.
        assertLanguageItem(
            suggestedItems, /* idx= */ 0, deviceLanguageLabel,
            /* isSelected= */ true, ListType.SUGGESTED);
        // The other item should be un-selected.
        assertLanguageItem(
            suggestedItems, /* idx= */ 1, testDisplayName,
            /* isSelected= */ false, ListType.SUGGESTED);
      });

  test(
      'Filter with search query, hide list and only matching items are visible',
      async () => {
        const testDisplayName = 'abc';
        const testDisplayName2 = 'abcde';
        const arcOptions: AppConfig = {
          type: AppType.kArc,
          supportedLocales: [
            {
              localeTag: 'test1',
              displayName: testDisplayName,
              nativeDisplayName: '',
            },
            {
              localeTag: 'test2',
              displayName: testDisplayName2,
              nativeDisplayName: '',
            },
          ],
          selectedLocale: {
            localeTag: 'test1',
            displayName: testDisplayName,
            nativeDisplayName: '',
          },
        };
        await addDialog(arcOptions, 'filter-with-search-query');

        setSearchQuery('abcde');
        // SuggestedList should be hidden.
        assertTrue(isHidden(getSuggestedList()));
        let filteredItems = getFilteredItems();
        // Only second item should be shown.
        assertEquals(1, filteredItems.length);
        assertLanguageItem(
            filteredItems, /* idx= */ 0, testDisplayName2,
            /* isSelected= */ false, ListType.FILTERED);
        assertTrue(isHidden(getNoSearchResultField()));

        setSearchQuery('abc');
        // Two items should be shown with the first one selected.
        filteredItems = getFilteredItems();
        assertEquals(2, filteredItems.length);
        assertLanguageItem(
            filteredItems, /* idx= */ 0, testDisplayName,
            /* isSelected= */ true, ListType.FILTERED);
        assertLanguageItem(
            filteredItems, /* idx= */ 1, testDisplayName2,
            /* isSelected= */ false, ListType.FILTERED);
        assertTrue(isHidden(getNoSearchResultField()));

        // No language matches search query, "no-search-result" text
        // should be shown.
        setSearchQuery('abd');
        assertTrue(isHidden(getFilteredList()));
        assertTrue(isVisible(getNoSearchResultField()));
      });

  test(
      'Confirm selection, selectedLocale should be set to test locale',
      async () => {
        const appId = 'confirm-selection';
        const testLocaleTag = 'testLocaleTag';
        const testDisplayName = 'testDisplayName';
        const arcOptions: AppConfig = {
          type: AppType.kArc,
          supportedLocales: [{
            localeTag: testLocaleTag,
            displayName: testDisplayName,
            nativeDisplayName: '',
          }],
        };
        await addDialog(arcOptions, appId);

        const filteredItems = getFilteredItems();
        assertEquals(1, filteredItems.length);
        filteredItems[0]!.shadowRoot!.querySelector<HTMLDivElement>(
                                         listItemId)!.click();
        // Test language should be selected.
        assertLanguageItem(
            filteredItems, /* idx= */ 0, testDisplayName,
            /* isSelected= */ true, ListType.FILTERED);
        confirmButton.click();
        await fakeHandler.flushPipesForTesting();

        const app = AppManagementStore.getInstance().data.apps[appId];
        assertEquals(testLocaleTag, app!.selectedLocale!.localeTag);
      });

  test(
      'Open dialog from AppsManagementPage and confirm selection, ' +
          'metrics recorded',
      async () => {
        const appId = 'open-dialog-from-apps-management-page';
        const testLocaleTag = 'testLocaleTag';
        const testDisplayName = 'testDisplayName';
        const arcOptions: AppConfig = {
          type: AppType.kArc,
          supportedLocales: [{
            localeTag: testLocaleTag,
            displayName: testDisplayName,
            nativeDisplayName: '',
          }],
        };
        await addDialog(
            arcOptions, appId, defaultPref,
            AppLanguageSelectionDialogEntryPoint.APPS_MANAGEMENT_PAGE);

        const filteredItems = getFilteredItems();
        assertEquals(1, filteredItems.length);
        filteredItems[0]!.shadowRoot!.querySelector<HTMLElement>(
                                         listItemId)!.click();
        // Test language should be selected.
        assertLanguageItem(
            filteredItems, /* idx= */ 0, testDisplayName,
            /* isSelected= */ true, ListType.FILTERED);
        confirmButton.click();
        await fakeHandler.flushPipesForTesting();

        assertEquals(
            1,
            metrics.count(
                'Arc.AppLanguageSwitch.AppsManagementPage.TargetLanguage',
                testLocaleTag));
      });
  test(
      'Open dialog from LanguagesPage and confirm selection, ' +
          'metrics recorded',
      async () => {
        const appId = 'open-dialog-from-languages-page';
        const testLocaleTag = 'testLocaleTag';
        const testDisplayName = 'testDisplayName';
        const arcOptions: AppConfig = {
          type: AppType.kArc,
          supportedLocales: [{
            localeTag: testLocaleTag,
            displayName: testDisplayName,
            nativeDisplayName: '',
          }],
        };
        await addDialog(
            arcOptions, appId, defaultPref,
            AppLanguageSelectionDialogEntryPoint.LANGUAGES_PAGE);

        const filteredItems = getFilteredItems();
        assertEquals(1, filteredItems.length);
        filteredItems[0]!.shadowRoot!.querySelector<HTMLElement>(
                                         listItemId)!.click();
        // Test language should be selected.
        assertLanguageItem(
            filteredItems, /* idx= */ 0, testDisplayName,
            /* isSelected= */ true, ListType.FILTERED);
        confirmButton.click();
        await fakeHandler.flushPipesForTesting();

        assertEquals(
            1,
            metrics.count(
                'Arc.AppLanguageSwitch.LanguagesPage.TargetLanguage',
                testLocaleTag));
      });

  test(
      'Last set app locale exists with no selected locale, ' +
          'display in suggested locales',
      async () => {
        const appId = 'last-set-app-locale-exists-with-no-selected-locale';
        const testLocaleTag = 'testLocaleTag';
        const testDisplayName = 'testDisplayName';
        const pref: chrome.settingsPrivate.PrefObject = {
          key: lastSetAppLocalePrefKey,
          type: chrome.settingsPrivate.PrefType.STRING,
          value: testLocaleTag,
        };
        const arcOptions: AppConfig = {
          type: AppType.kArc,
          supportedLocales: [{
            localeTag: testLocaleTag,
            displayName: testDisplayName,
            nativeDisplayName: '',
          }],
        };
        await addDialog(arcOptions, appId, pref);

        // Suggested items should contain device language and last set app
        // locale.
        const suggestedItems = getSuggestedItems();
        assertEquals(2, suggestedItems.length);
        assertLanguageItem(
            suggestedItems, /* idx= */ 0, deviceLanguageLabel,
            /* isSelected= */ true, ListType.SUGGESTED);
        assertLanguageItem(
            suggestedItems, /* idx= */ 1, testDisplayName,
            /* isSelected= */ false, ListType.SUGGESTED);
        // Filtered list should be hidden.
        assertTrue(
            isHidden(getFilteredList()), '#filteredItemsList is not hidden');
      });

  test(
      'Last set app locale is same with selected locale, no duplicates',
      async () => {
        const appId = 'last-set-app-locale-is-same-with-selected-locale';
        const testLocaleTag = 'testLocaleTag';
        const testDisplayName = 'testDisplayName';
        const pref: chrome.settingsPrivate.PrefObject = {
          key: lastSetAppLocalePrefKey,
          type: chrome.settingsPrivate.PrefType.STRING,
          value: testLocaleTag,
        };
        const arcOptions: AppConfig = {
          type: AppType.kArc,
          supportedLocales: [{
            localeTag: testLocaleTag,
            displayName: testDisplayName,
            nativeDisplayName: '',
          }],
          selectedLocale: {
            localeTag: testLocaleTag,
            displayName: testDisplayName,
            nativeDisplayName: '',
          },
        };
        await addDialog(arcOptions, appId, pref);

        // Suggested items should contain device language and selected locale.
        const suggestedItems = getSuggestedItems();
        assertEquals(2, suggestedItems.length);
        assertLanguageItem(
            suggestedItems, /* idx= */ 0, deviceLanguageLabel,
            /* isSelected= */ false, ListType.SUGGESTED);
        assertLanguageItem(
            suggestedItems, /* idx= */ 1, testDisplayName,
            /* isSelected= */ true, ListType.SUGGESTED);
        // Filtered list should be hidden.
        assertTrue(
            isHidden(getFilteredList()), '#filteredItemsList is not hidden');
      });

  test(
      'Last set app locale is different with selected locale, ' +
          'display 3 locales in sugested locales',
      async () => {
        const appId = 'last-set-app-locale-is-different-with-selected-locale';
        const testLocaleTag = 'testLocaleTag';
        const testLocaleTag2 = 'testLocaleTag2';
        const testDisplayName = 'testDisplayName';
        const testDisplayName2 = 'testDisplayName2';
        const pref: chrome.settingsPrivate.PrefObject = {
          key: lastSetAppLocalePrefKey,
          type: chrome.settingsPrivate.PrefType.STRING,
          value: testLocaleTag2,
        };
        const arcOptions: AppConfig = {
          type: AppType.kArc,
          supportedLocales: [
            {
              localeTag: testLocaleTag,
              displayName: testDisplayName,
              nativeDisplayName: '',
            },
            {
              localeTag: testLocaleTag2,
              displayName: testDisplayName2,
              nativeDisplayName: '',
            },
          ],
          selectedLocale: {
            localeTag: testLocaleTag,
            displayName: testDisplayName,
            nativeDisplayName: '',
          },
        };
        await addDialog(arcOptions, appId, pref);

        // Suggested items should contain device language, selected locale and
        // last set app locale.
        const suggestedItems = getSuggestedItems();
        assertEquals(3, suggestedItems.length);
        assertLanguageItem(
            suggestedItems, /* idx= */ 0, deviceLanguageLabel,
            /* isSelected= */ false, ListType.SUGGESTED);
        assertLanguageItem(
            suggestedItems, /* idx= */ 1, testDisplayName,
            /* isSelected= */ true, ListType.SUGGESTED);
        assertLanguageItem(
            suggestedItems, /* idx= */ 2, testDisplayName2,
            /* isSelected= */ false, ListType.SUGGESTED);
        // Filtered list should be hidden.
        assertTrue(
            isHidden(getFilteredList()), '#filteredItemsList is not hidden');
      });
});