chromium/chrome/test/data/webui/chromeos/settings/controls/v2/settings_dropdown_v2_test.ts

// Copyright 2024 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/os_settings.js';

import {SettingsDropdownV2Element} from 'chrome://os-settings/os_settings.js';
import {assertEquals, assertFalse, assertNotEquals, assertThrows, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
import {eventToPromise, isVisible} from 'chrome://webui-test/test_util.js';

import {clearBody} from '../../utils.js';

suite(SettingsDropdownV2Element.is, () => {
  let dropdownElement: SettingsDropdownV2Element;
  let internalSelectElement: HTMLSelectElement;

  const testOptions = [
    {label: 'Lion', value: 1},
    {label: 'Tiger', value: 2},
    {label: 'Bear', value: 3},
  ];

  const fakeNumberPrefObject = {
    key: 'settings.animal',
    type: chrome.settingsPrivate.PrefType.NUMBER,
    value: 1,
  };

  function simulateSelectAction(value: string|number): void {
    internalSelectElement.value = value.toString();
    internalSelectElement.dispatchEvent(new CustomEvent('change'));
  }

  function assertOptionSelected(expectedValue: string|number): void {
    assertEquals(expectedValue.toString(), internalSelectElement.value);

    const selectedOptionIndex =
        Array.from(internalSelectElement.options).findIndex((option) => {
          return expectedValue.toString() === option.value;
        });
    assertEquals(selectedOptionIndex, internalSelectElement.selectedIndex);

    const selectedOption = internalSelectElement.options[selectedOptionIndex];
    assertTrue(
        !!selectedOption, `No matching option for value: ${expectedValue}`);
    assertTrue(selectedOption.selected, 'Option is not selected');
  }

  function assertNoOptionSelected(): void {
    assertEquals('', internalSelectElement.value);
    assertEquals(-1, internalSelectElement.selectedIndex);
  }

  setup(async () => {
    clearBody();
    dropdownElement = document.createElement(SettingsDropdownV2Element.is);
    dropdownElement.options = testOptions;
    document.body.appendChild(dropdownElement);
    internalSelectElement = dropdownElement.$.select;
    await flushTasks();
  });

  suite('disabled property', () => {
    test('should reflect to attribute', () => {
      // `disabled` is false by default.
      assertFalse(dropdownElement.hasAttribute('disabled'));

      dropdownElement.disabled = true;
      assertTrue(dropdownElement.hasAttribute('disabled'));

      dropdownElement.disabled = false;
      assertFalse(dropdownElement.hasAttribute('disabled'));
    });

    test('is true if pref is enforced', () => {
      dropdownElement.pref = {...fakeNumberPrefObject};
      assertFalse(dropdownElement.disabled);

      dropdownElement.pref = {
        ...fakeNumberPrefObject,
        enforcement: chrome.settingsPrivate.Enforcement.ENFORCED,
      };
      assertTrue(dropdownElement.disabled);
    });

    test('cannot be overridden if pref is enforced', () => {
      dropdownElement.pref = {
        ...fakeNumberPrefObject,
        enforcement: chrome.settingsPrivate.Enforcement.ENFORCED,
      };
      assertTrue(dropdownElement.disabled);

      // Attempt to force enable the element. Element should still be disabled
      // since the pref is enforced.
      dropdownElement.disabled = false;
      assertTrue(dropdownElement.disabled);
    });
  });

  test('Internal select is disabled if there are no menu options', () => {
    assertTrue(dropdownElement.options.length > 0);
    assertFalse(internalSelectElement.disabled);

    dropdownElement.options = [];
    assertTrue(internalSelectElement.disabled);
  });

  test('Hidden options are not shown', async () => {
    const testOptionsWithHiddenAttr = [
      {label: 'Lion', value: 1, hidden: false},
      {
        label: 'Tiger',
        value: 2,
        hidden: true,
      },
      {label: 'Bear', value: 3},
    ];
    dropdownElement.options = testOptionsWithHiddenAttr;
    await flushTasks();

    testOptionsWithHiddenAttr.forEach((option, index) => {
      assertEquals(
          !!option.hidden, internalSelectElement.options[index]!.hidden);
    });
  });

  suite('with pref', () => {
    setup(async () => {
      dropdownElement.pref = {...fakeNumberPrefObject};
      await flushTasks();
    });

    suite('Pref type validation', () => {
      [{
        prefType: chrome.settingsPrivate.PrefType.STRING,
        testValue: 'foo',
        isValid: true,
      },
       {
         prefType: chrome.settingsPrivate.PrefType.NUMBER,
         testValue: 1,
         isValid: true,
       },
       {
         prefType: chrome.settingsPrivate.PrefType.DICTIONARY,
         testValue: {},
         isValid: false,
       },
       {
         prefType: chrome.settingsPrivate.PrefType.BOOLEAN,
         testValue: true,
         isValid: false,
       },
       {
         prefType: chrome.settingsPrivate.PrefType.LIST,
         testValue: [],
         isValid: false,
       },
       {
         prefType: chrome.settingsPrivate.PrefType.URL,
         testValue: 'bar',
         isValid: false,
       },
      ].forEach(({prefType, testValue, isValid}) => {
        test(
            `${prefType} pref type is ${isValid ? 'valid' : 'invalid'}`, () => {
              function validatePref() {
                dropdownElement.pref = {
                  key: 'settings.sample',
                  type: prefType,
                  value: testValue,
                };
                dropdownElement.validatePref();
              }

              if (isValid) {
                validatePref();
              } else {
                assertThrows(validatePref);
              }
            });
      });
    });

    test('Pref value updates the selected option', async () => {
      for (const testOption of testOptions) {
        dropdownElement.set('pref.value', testOption.value);
        await flushTasks();
        assertOptionSelected(testOption.value);
      }
    });

    test(
        'No option is selected if no matching option for pref value',
        async () => {
          assertOptionSelected(1);

          dropdownElement.set('pref.value', 9001);
          await flushTasks();
          assertNoOptionSelected();

          dropdownElement.set('pref.value', 2);
          await flushTasks();
          assertOptionSelected(2);
        });

    test('Pref value syncs to the "value" property', () => {
      dropdownElement.set('pref.value', 2);
      assertEquals(2, dropdownElement.value);

      dropdownElement.set('pref.value', 3);
      assertEquals(3, dropdownElement.value);

      dropdownElement.set('pref.value', 9001);
      assertEquals(9001, dropdownElement.value);
    });

    test('policy indicator does not show if pref is not enforced', () => {
      const policyIndicator =
          dropdownElement.shadowRoot!.querySelector('cr-policy-pref-indicator');
      assertFalse(isVisible(policyIndicator));
    });

    test('policy indicator shows if pref is enforced', async () => {
      dropdownElement.pref = {
        ...fakeNumberPrefObject,
        enforcement: chrome.settingsPrivate.Enforcement.ENFORCED,
        controlledBy: chrome.settingsPrivate.ControlledBy.DEVICE_POLICY,
      };
      await flushTasks();

      const policyIndicator =
          dropdownElement.shadowRoot!.querySelector('cr-policy-pref-indicator');
      assertTrue(isVisible(policyIndicator));
    });

    test('Selecting an option dispatches pref change event', async () => {
      for (const testOption of testOptions) {
        const prefChangeEventPromise =
            eventToPromise('user-action-setting-pref-change', window);
        const value = testOption.value;
        simulateSelectAction(value);
        assertOptionSelected(value);

        const event = await prefChangeEventPromise;
        assertEquals(fakeNumberPrefObject.key, event.detail.prefKey);
        assertEquals(value, event.detail.value);
      }
    });

    test(
        'Selecting an option does not update the pref value directly',
        async () => {
          const initialPrefValue = dropdownElement.pref!.value;

          const prefChangeEventPromise =
              eventToPromise('user-action-setting-pref-change', window);
          const value = testOptions[0]!.value;
          simulateSelectAction(value);
          assertOptionSelected(value);
          await prefChangeEventPromise;

          // Local pref object should be treated as immutable data and should
          // not be updated directly.
          assertEquals(initialPrefValue, dropdownElement.pref!.value);
        });

    test('Selecting an option dispatches change event', async () => {
      for (const testOption of testOptions) {
        const value = testOption.value;
        const changeEventPromise = eventToPromise('change', window);
        simulateSelectAction(value);
        assertOptionSelected(value);
        const event = await changeEventPromise;
        assertEquals(value, event.detail);
        // Event should not pass the shadow DOM boundary.
        assertFalse(event.composed);
      }
    });
  });

  suite('without pref', () => {
    test('Changing value updates the selected option', async () => {
      for (const testOption of testOptions) {
        dropdownElement.value = testOption.value;
        await flushTasks();
        assertOptionSelected(testOption.value);
      }
    });

    test('No option is selected if no matching option for value', async () => {
      assertNoOptionSelected();

      dropdownElement.value = 1;
      await flushTasks();
      assertOptionSelected(1);

      dropdownElement.value = 9001;
      await flushTasks();
      assertNoOptionSelected();
    });

    test('Selecting an option updates value property', () => {
      for (const testOption of testOptions) {
        const value = testOption.value;
        simulateSelectAction(value);
        assertOptionSelected(value);
        assertEquals(value, dropdownElement.value);
      }
    });

    test('Selecting an option dispatches change event', async () => {
      for (const testOption of testOptions) {
        const value = testOption.value;
        const changeEventPromise = eventToPromise('change', window);
        simulateSelectAction(value);
        assertOptionSelected(value);
        const event = await changeEventPromise;
        assertEquals(value, event.detail);
        // Event should not pass the shadow DOM boundary.
        assertFalse(event.composed);
      }
    });
  });

  suite('focus()', () => {
    test('should focus the internal select element', () => {
      assertNotEquals(
          internalSelectElement, dropdownElement.shadowRoot!.activeElement);
      dropdownElement.focus();
      assertEquals(
          internalSelectElement, dropdownElement.shadowRoot!.activeElement);
    });
  });

  suite('for a11y', () => {
    test('ariaLabel property should apply to internal select', () => {
      const ariaLabel = 'A11y label';
      dropdownElement.ariaLabel = ariaLabel;
      assertEquals(ariaLabel, internalSelectElement.getAttribute('aria-label'));
    });

    test('ariaLabel property does not reflect to attribute', () => {
      const ariaLabel = 'A11y label';
      dropdownElement.ariaLabel = ariaLabel;
      assertFalse(dropdownElement.hasAttribute('aria-label'));
    });

    test('ariaDescription property should apply to internal select', () => {
      const ariaDescription = 'A11y description';
      dropdownElement.ariaDescription = ariaDescription;
      assertEquals(
          ariaDescription,
          internalSelectElement.getAttribute('aria-description'));
    });

    test('ariaDescription property does not reflect to attribute', () => {
      const ariaDescription = 'A11y description';
      dropdownElement.ariaDescription = ariaDescription;
      assertFalse(dropdownElement.hasAttribute('aria-description'));
    });
  });
});