chromium/chrome/test/data/webui/chromeos/settings/os_bluetooth_page/os_bluetooth_summary_test.ts

// Copyright 2021 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 'chrome://os-settings/lazy_load.js';

import {CrToggleElement, IronIconElement, OsBluetoothDevicesSubpageBrowserProxyImpl, Router, routes, SettingsBluetoothSummaryElement} from 'chrome://os-settings/os_settings.js';
import {setBluetoothConfigForTesting} from 'chrome://resources/ash/common/bluetooth/cros_bluetooth_config.js';
import {setHidPreservingControllerForTesting} from 'chrome://resources/ash/common/bluetooth/hid_preserving_bluetooth_state_controller.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {mojoString16ToString} from 'chrome://resources/js/mojo_type_util.js';
import {BluetoothSystemProperties, BluetoothSystemState, DeviceConnectionState, SystemPropertiesObserverInterface} from 'chrome://resources/mojo/chromeos/ash/services/bluetooth_config/public/mojom/cros_bluetooth_config.mojom-webui.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {assertEquals, assertFalse, assertNotEquals, assertNull, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {createDefaultBluetoothDevice, FakeBluetoothConfig} from 'chrome://webui-test/cr_components/chromeos/bluetooth/fake_bluetooth_config.js';
import {FakeHidPreservingBluetoothStateController} from 'chrome://webui-test/cr_components/chromeos/bluetooth/fake_hid_preserving_bluetooth_state_controller.js';
import {flushTasks, waitAfterNextRender} from 'chrome://webui-test/polymer_test_util.js';
import {eventToPromise} from 'chrome://webui-test/test_util.js';

import {TestOsBluetoothDevicesSubpageBrowserProxy} from './test_os_bluetooth_subpage_browser_proxy.js';

suite('<os-settings-bluetooth-summary>', () => {
  let bluetoothConfig: FakeBluetoothConfig;
  let bluetoothSummary: SettingsBluetoothSummaryElement;
  let propertiesObserver: SystemPropertiesObserverInterface;
  let browserProxy: TestOsBluetoothDevicesSubpageBrowserProxy;
  let hidPreservingController: FakeHidPreservingBluetoothStateController;

  setup(() => {
    bluetoothConfig = new FakeBluetoothConfig();
    setBluetoothConfigForTesting(bluetoothConfig);
  });

  teardown(() => {
    bluetoothSummary.remove();
    browserProxy.reset();
    Router.getInstance().resetRouteForTesting();
  });

  async function init(isBluetoothDisconnectWarningEnabled: boolean = false) {
    if (isBluetoothDisconnectWarningEnabled) {
      loadTimeData.overrideValues({'bluetoothDisconnectWarningFlag': true});
      hidPreservingController = new FakeHidPreservingBluetoothStateController();
      hidPreservingController.setBluetoothConfigForTesting(bluetoothConfig);
      setHidPreservingControllerForTesting(hidPreservingController);
    } else {
      loadTimeData.overrideValues({'bluetoothDisconnectWarningFlag': false});
    }

    browserProxy = new TestOsBluetoothDevicesSubpageBrowserProxy();
    OsBluetoothDevicesSubpageBrowserProxyImpl.setInstanceForTesting(
        browserProxy);
    bluetoothSummary = document.createElement('os-settings-bluetooth-summary');
    document.body.appendChild(bluetoothSummary);
    flush();

    propertiesObserver = {
      /**
       * SystemPropertiesObserverInterface override properties
       */
      onPropertiesUpdated(properties: BluetoothSystemProperties) {
        bluetoothSummary.systemProperties = properties;
      },
    };
    bluetoothConfig.observeSystemProperties(propertiesObserver);
  }

  test('Toggle Bluetooth with bluetoothDisconnectWarningFlag on', async () => {
    await flushTasks();
    await init(/* isBluetoothDisconnectWarningEnabled= */ true);
    bluetoothConfig.setSystemState(BluetoothSystemState.kDisabled);
    await flushTasks();

    const enableBluetoothToggle =
        bluetoothSummary.shadowRoot!.querySelector<CrToggleElement>(
            '#enableBluetoothToggle');
    assertTrue(!!enableBluetoothToggle);

    const enableBluetooth = async () => {
      assertTrue(
          bluetoothSummary.systemProperties.systemState ===
          BluetoothSystemState.kDisabled);

      // Simulate clicking toggle.
      enableBluetoothToggle.click();
      await flushTasks();

      // Toggle should be on since systemState is enabling.
      assertTrue(
          bluetoothSummary.systemProperties.systemState ===
          BluetoothSystemState.kEnabling);

      // Mock operation success.
      bluetoothConfig.completeSetBluetoothEnabledState(/*success=*/ true);
      await flushTasks();
      assertTrue(
          bluetoothSummary.systemProperties.systemState ===
          BluetoothSystemState.kEnabled);
    };

    await enableBluetooth();
    assertEquals(hidPreservingController.getDialogShownCount(), 0);

    // Disable bluetooth and simulate showing dialog, with user electing
    // to continue disabling Bluetooth.
    hidPreservingController.setShouldShowWarningDialog(true);
    enableBluetoothToggle.click();
    await flushTasks();

    assertTrue(enableBluetoothToggle.checked);
    assertEquals(hidPreservingController.getDialogShownCount(), 1);
    assertTrue(
        bluetoothSummary.systemProperties.systemState ===
        BluetoothSystemState.kEnabled);
    hidPreservingController.completeShowDialog(true);
    await flushTasks();

    assertFalse(enableBluetoothToggle.checked);
    assertTrue(
        bluetoothSummary.systemProperties.systemState ===
        BluetoothSystemState.kDisabling);
    bluetoothConfig.completeSetBluetoothEnabledState(/*success=*/ true);
    await flushTasks();
    await enableBluetooth();
    assertEquals(hidPreservingController.getDialogShownCount(), 1);
    assertTrue(enableBluetoothToggle.checked);

    // Disable Bluetooth and simulate showing dialog with user selecting
    // to keep current bluetooth state.
    enableBluetoothToggle.click();
    await flushTasks();

    assertTrue(enableBluetoothToggle.checked);
    assertEquals(hidPreservingController.getDialogShownCount(), 2);
    assertTrue(
        bluetoothSummary.systemProperties.systemState ===
        BluetoothSystemState.kEnabled);
    hidPreservingController.completeShowDialog(false);

    await flushTasks();
    assertTrue(enableBluetoothToggle.checked);
    assertTrue(
        bluetoothSummary.systemProperties.systemState ===
        BluetoothSystemState.kEnabled);
  });

  test('Button is focused after returning from devices subpage', async () => {
    await init();
    bluetoothConfig.setBluetoothEnabledState(/*enabled=*/ true);
    await flushTasks();
    const iconButton =
        bluetoothSummary.shadowRoot!.querySelector<HTMLButtonElement>(
            '#arrowIconButton');
    assertTrue(!!iconButton);

    iconButton.click();
    assertEquals(routes.BLUETOOTH_DEVICES, Router.getInstance().currentRoute);
    assertNotEquals(
        iconButton, bluetoothSummary.shadowRoot!.activeElement,
        'subpage icon should not be focused');

    // Navigate back to the top-level page.
    const windowPopstatePromise = eventToPromise('popstate', window);
    Router.getInstance().navigateToPreviousRoute();
    await windowPopstatePromise;
    await waitAfterNextRender(bluetoothSummary);

    // Check that |iconButton| has been focused.
    assertEquals(
        iconButton, bluetoothSummary.shadowRoot!.activeElement,
        'subpage icon should be focused');
  });

  test('Toggle button creation and a11y', async () => {
    await init();
    bluetoothConfig.setSystemState(BluetoothSystemState.kEnabled);
    await flushTasks();
    let a11yMessagesEventPromise =
        eventToPromise('cr-a11y-announcer-messages-sent', document.body);

    const toggle = bluetoothSummary.shadowRoot!.querySelector<CrToggleElement>(
        '#enableBluetoothToggle');
    assertTrue(!!toggle);
    assertTrue(toggle.checked);

    toggle.click();
    let a11yMessagesEvent = await a11yMessagesEventPromise;
    assertTrue(a11yMessagesEvent.detail.messages.includes(
        bluetoothSummary.i18n('bluetoothDisabledA11YLabel')));

    a11yMessagesEventPromise =
        eventToPromise('cr-a11y-announcer-messages-sent', document.body);
    toggle.click();

    a11yMessagesEvent = await a11yMessagesEventPromise;
    assertTrue(a11yMessagesEvent.detail.messages.includes(
        bluetoothSummary.i18n('bluetoothEnabledA11YLabel')));
  });

  test('Toggle button states', async () => {
    await init();
    bluetoothConfig.setSystemState(BluetoothSystemState.kDisabled);
    await flushTasks();
    assertEquals(0, browserProxy.getShowBluetoothRevampHatsSurveyCount());

    const getPairNewDeviceBtn = () =>
        bluetoothSummary.shadowRoot!.querySelector('#pairNewDeviceBtn');

    const enableBluetoothToggle =
        bluetoothSummary.shadowRoot!.querySelector<CrToggleElement>(
            '#enableBluetoothToggle');
    assertTrue(!!enableBluetoothToggle);
    assertFalse(enableBluetoothToggle.checked);

    assertNull(getPairNewDeviceBtn());

    // Simulate clicking toggle.
    enableBluetoothToggle.click();
    await flushTasks();

    // Toggle should be on since systemState is enabling.
    assertTrue(enableBluetoothToggle.checked);
    assertEquals(
        1, browserProxy.getShowBluetoothRevampHatsSurveyCount(),
        'Count failed to increase');

    // Mock operation failing.
    bluetoothConfig.completeSetBluetoothEnabledState(/*success=*/ false);
    await flushTasks();

    // Toggle should be off again.
    assertFalse(enableBluetoothToggle.checked);
    assertEquals(
        1, browserProxy.getShowBluetoothRevampHatsSurveyCount(),
        'Count failed to remain the same');
    assertNull(getPairNewDeviceBtn());

    // Click again.
    enableBluetoothToggle.click();
    await flushTasks();

    // Toggle should be on since systemState is enabling.
    assertTrue(enableBluetoothToggle.checked);
    assertEquals(
        2, browserProxy.getShowBluetoothRevampHatsSurveyCount(),
        'Count failed to increase');
    assertNull(getPairNewDeviceBtn());

    // Mock operation success.
    bluetoothConfig.completeSetBluetoothEnabledState(/*success=*/ true);
    await flushTasks();

    // Toggle should still be on.
    assertTrue(enableBluetoothToggle.checked);
    assertEquals(
        2, browserProxy.getShowBluetoothRevampHatsSurveyCount(),
        'Count failed to remain the same');
    assertTrue(!!getPairNewDeviceBtn());

    // Mock systemState becoming unavailable.
    bluetoothConfig.setSystemState(BluetoothSystemState.kUnavailable);
    await flushTasks();
    assertTrue(enableBluetoothToggle.disabled);
    assertFalse(enableBluetoothToggle.checked);
    assertEquals(
        2, browserProxy.getShowBluetoothRevampHatsSurveyCount(),
        'Count failed to remain the same');
  });

  test('UI states test', async () => {
    await init();

    bluetoothConfig.setSystemState(BluetoothSystemState.kDisabled);
    await flushTasks();

    // Simulate device state is disabled.
    const bluetoothSecondaryLabel =
        bluetoothSummary.shadowRoot!.querySelector('#bluetoothSecondaryLabel');
    assertTrue(!!bluetoothSecondaryLabel);
    const getBluetoothArrowIconBtn = () =>
        bluetoothSummary.shadowRoot!.querySelector('#arrowIconButton');
    const getBluetoothStatusIcon = () => {
      const statusIcon =
          bluetoothSummary.shadowRoot!.querySelector<IronIconElement>(
              '#statusIcon');
      assertTrue(!!statusIcon);
      return statusIcon;
    };
    const getSecondaryLabel = () => bluetoothSecondaryLabel.textContent?.trim();
    const getPairNewDeviceBtn = () =>
        bluetoothSummary.shadowRoot!.querySelector('#pairNewDeviceBtn');

    assertNull(getBluetoothArrowIconBtn());
    assertTrue(!!getBluetoothStatusIcon());
    assertNull(getPairNewDeviceBtn());

    assertEquals(
        bluetoothSummary.i18n('bluetoothSummaryPageOff'), getSecondaryLabel());
    assertEquals(
        'os-settings:bluetooth-disabled', getBluetoothStatusIcon().icon);

    bluetoothConfig.setBluetoothEnabledState(/*enabled=*/ true);
    await flushTasks();

    assertTrue(!!getBluetoothArrowIconBtn());
    assertNull(getPairNewDeviceBtn());
    // Bluetooth Icon should be default because no devices are connected.
    assertEquals('cr:bluetooth', getBluetoothStatusIcon().icon);

    bluetoothConfig.completeSetBluetoothEnabledState(/*success=*/ true);
    await flushTasks();

    assertTrue(!!getBluetoothArrowIconBtn());
    assertTrue(!!getPairNewDeviceBtn());
    // Bluetooth Icon should be default because no devices are connected.
    assertEquals('cr:bluetooth', getBluetoothStatusIcon().icon);

    const device1 = createDefaultBluetoothDevice(
        /*id=*/ '123456789', /*publicName=*/ 'BeatsX',
        /*connectionState=*/
        DeviceConnectionState.kConnected,
        /*opt_nickname=*/ 'device1');
    const device2 = createDefaultBluetoothDevice(
        /*id=*/ '987654321', /*publicName=*/ 'MX 3',
        /*connectionState=*/
        DeviceConnectionState.kConnected);
    const device3 = createDefaultBluetoothDevice(
        /*id=*/ '456789', /*publicName=*/ 'Radio head',
        /*connectionState=*/
        DeviceConnectionState.kConnected,
        /*opt_nickname=*/ 'device3');

    const mockPairedBluetoothDeviceProperties = [
      device1,
      device2,
      device3,
    ];

    // Simulate 3 connected devices.
    bluetoothConfig.appendToPairedDeviceList(
        mockPairedBluetoothDeviceProperties);
    await flushTasks();

    assertEquals(
        'os-settings:bluetooth-connected', getBluetoothStatusIcon().icon);
    assertEquals(
        bluetoothSummary.i18n(
            'bluetoothSummaryPageTwoOrMoreDevicesDescription',
            device1.nickname!, mockPairedBluetoothDeviceProperties.length - 1),
        getSecondaryLabel());

    // Simulate 2 connected devices.
    bluetoothConfig.removePairedDevice(device3);
    await flushTasks();

    assertEquals(
        bluetoothSummary.i18n(
            'bluetoothSummaryPageTwoDevicesDescription', device1.nickname!,
            mojoString16ToString(device2.deviceProperties.publicName)),
        getSecondaryLabel());

    // Simulate a single connected device.
    bluetoothConfig.removePairedDevice(device2);
    await flushTasks();

    assertEquals(device1.nickname, getSecondaryLabel());

    /// Simulate no connected device.
    bluetoothConfig.removePairedDevice(device1);
    await flushTasks();

    assertEquals(
        bluetoothSummary.i18n('bluetoothSummaryPageOn'), getSecondaryLabel());
    assertEquals('cr:bluetooth', getBluetoothStatusIcon().icon);
    assertTrue(!!getPairNewDeviceBtn());

    // Mock systemState becoming unavailable.
    bluetoothConfig.setSystemState(BluetoothSystemState.kUnavailable);
    await flushTasks();
    assertNull(getBluetoothArrowIconBtn());
    assertNull(getPairNewDeviceBtn());
    assertEquals(
        bluetoothSummary.i18n('bluetoothSummaryPageOff'), getSecondaryLabel());
    assertEquals(
        'os-settings:bluetooth-disabled', getBluetoothStatusIcon().icon);
  });

  test('start-pairing is fired on pairNewDeviceBtn click', async () => {
    await init();
    bluetoothConfig.setBluetoothEnabledState(/*enabled=*/ true);
    await flushTasks();

    bluetoothConfig.completeSetBluetoothEnabledState(/*success=*/ true);
    await flushTasks();

    const toggleBluetoothPairingUiPromise =
        eventToPromise('start-pairing', bluetoothSummary);
    const getPairNewDeviceBtn = () => {
      const button =
          bluetoothSummary.shadowRoot!.querySelector<HTMLButtonElement>(
              '#pairNewDeviceBtn');
      assertTrue(!!button);
      return button;
    };
    getPairNewDeviceBtn().click();

    await toggleBluetoothPairingUiPromise;
  });

  test('Secondary user', async () => {
    const primaryUserEmail = '[email protected]';
    loadTimeData.overrideValues({
      isSecondaryUser: true,
      primaryUserEmail,
    });
    await init();

    bluetoothConfig.setBluetoothEnabledState(/*enabled=*/ true);
    await flushTasks();
    const bluetoothSummaryPrimary =
        bluetoothSummary.shadowRoot!.querySelector('#bluetoothSummary');
    const bluetoothSummarySecondary =
        bluetoothSummary.shadowRoot!.querySelector('#bluetoothSummarySeconday');
    const bluetoothSummarySecondaryText =
        bluetoothSummary.shadowRoot!.querySelector(
            '#bluetoothSummarySecondayText');

    assertNull(bluetoothSummaryPrimary);
    assertTrue(!!bluetoothSummarySecondary);
    assertTrue(!!bluetoothSummarySecondaryText);

    assertEquals(
        bluetoothSummary.i18n(
            'bluetoothPrimaryUserControlled', primaryUserEmail),
        bluetoothSummarySecondaryText.textContent?.trim());
  });

  test('Route to summary page', async () => {
    await init();
    assertEquals(0, browserProxy.getShowBluetoothRevampHatsSurveyCount());
    Router.getInstance().navigateTo(routes.BLUETOOTH);
    assertEquals(
        1, browserProxy.getShowBluetoothRevampHatsSurveyCount(),
        'Count failed to increase');
  });
});