chromium/chrome/test/data/webui/chromeos/settings/os_bluetooth_page/os_bluetooth_device_detail_subpage_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/lazy_load.js';

import {OsSettingsSubpageElement, SettingsBluetoothDeviceDetailSubpageElement, SettingsBluetoothTrueWirelessImagesElement} from 'chrome://os-settings/lazy_load.js';
import {CrLinkRowElement, OsBluetoothDevicesSubpageBrowserProxyImpl, Router, routes} from 'chrome://os-settings/os_settings.js';
import {setBluetoothConfigForTesting} from 'chrome://resources/ash/common/bluetooth/cros_bluetooth_config.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {AudioOutputCapability, BluetoothSystemProperties, DeviceBatteryInfo, DeviceConnectionState, DeviceType, 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 {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-device-detail-subpage>', () => {
  let bluetoothConfig: FakeBluetoothConfig;
  let bluetoothDeviceDetailPage: SettingsBluetoothDeviceDetailSubpageElement;
  let propertiesObserver: SystemPropertiesObserverInterface;
  let browserProxy: TestOsBluetoothDevicesSubpageBrowserProxy;

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

  function init(): void {
    browserProxy = new TestOsBluetoothDevicesSubpageBrowserProxy();
    OsBluetoothDevicesSubpageBrowserProxyImpl.setInstanceForTesting(
        browserProxy);

    bluetoothDeviceDetailPage =
        document.createElement('os-settings-bluetooth-device-detail-subpage');
    document.body.appendChild(bluetoothDeviceDetailPage);
    flush();

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

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

  function getBluetoothForgetBtn(): HTMLButtonElement|null {
    return bluetoothDeviceDetailPage.shadowRoot!
        .querySelector<HTMLButtonElement>('#forgetBtn');
  }

  function getBluetoothConnectDisconnectBtn(): HTMLButtonElement | null {
    return bluetoothDeviceDetailPage.shadowRoot!
        .querySelector<HTMLButtonElement>('#connectDisconnectBtn');
  }

  function navigateToDeviceDetailPage(id: string): void {
    const params = new URLSearchParams();
    params.append('id', id);
    Router.getInstance().navigateTo(routes.BLUETOOTH_DEVICE_DETAIL, params);
  }

  function getConnectionFailedText(): HTMLElement|null {
    return bluetoothDeviceDetailPage.shadowRoot!.querySelector(
        '#connectionFailed');
  }

  function getTrueWirelessImages():
    SettingsBluetoothTrueWirelessImagesElement | null {
    return bluetoothDeviceDetailPage.shadowRoot!.querySelector(
        '#trueWirelessImages');
  }

  function getChangeMouseSettings(): CrLinkRowElement|null {
    return bluetoothDeviceDetailPage.shadowRoot!
        .querySelector<CrLinkRowElement>('#changeMouseSettings');
  }

  function getChangeKeyboardSettings():HTMLButtonElement|null {
    return bluetoothDeviceDetailPage.shadowRoot!
        .querySelector<HTMLButtonElement>('#changeKeyboardSettings');
  }

  function getBluetoothStateText(): HTMLElement {
    const text =
        bluetoothDeviceDetailPage.shadowRoot!.querySelector<HTMLElement>(
        '#bluetoothStateText');
    assertTrue(!!text);
    return text;
  }

  function getDefaultDeviceBatteryInfo(): DeviceBatteryInfo {
    return {
        defaultProperties: undefined,
        leftBudInfo: undefined,
        rightBudInfo: undefined,
        caseInfo: undefined,
    };
  }

  test('Error text is not shown after navigating away from page', async () => {
    init();
    bluetoothConfig.setBluetoothEnabledState(/*enabled=*/ true);
    const id = '12345/6789&';
    const device1 = createDefaultBluetoothDevice(
        id,
        /*publicName=*/ 'BeatsX',
        /*connectionState=*/
        DeviceConnectionState.kNotConnected,
        /*opt_nickname=*/ 'device1',
        /*opt_audioCapability=*/
        AudioOutputCapability.kCapableOfAudioOutput,
        /*opt_deviceType=*/ DeviceType.kMouse);

      device1.deviceProperties.batteryInfo = {
        ...getDefaultDeviceBatteryInfo(),
      defaultProperties: {batteryPercentage: 90},
    };

    bluetoothConfig.appendToPairedDeviceList([device1]);
    await flushTasks();

    navigateToDeviceDetailPage(id);
    await flushTasks();

    // Try to connect.
    getBluetoothConnectDisconnectBtn()!.click();
    await flushTasks();
    bluetoothConfig.completeConnect(/*success=*/ false);
    await flushTasks();
    assertTrue(!!getConnectionFailedText());

    navigateToDeviceDetailPage(id);
    await flushTasks();
    assertNull(getConnectionFailedText());
  });

  test('Managed by enterprise icon', async () => {
    init();
    bluetoothConfig.setBluetoothEnabledState(/*enabled=*/ true);

    const getManagedIcon = () =>
       bluetoothDeviceDetailPage.shadowRoot!.querySelector(
          '#managedIcon');

    const device = createDefaultBluetoothDevice(
        /*id=*/ '12345/6789&',
        /*publicName=*/ 'BeatsX',
        /*connectionState=*/
        DeviceConnectionState.kConnected,
        /*opt_nickname=*/ 'device1',
        /*opt_audioCapability=*/
        AudioOutputCapability.kCapableOfAudioOutput,
        /*opt_deviceType=*/ DeviceType.kMouse,
        /*opt_isBlockedByPolicy=*/ true);

    bluetoothConfig.appendToPairedDeviceList([device]);
    await flushTasks();

    navigateToDeviceDetailPage('12345/6789&');

    await flushTasks();
    assertTrue(!!getManagedIcon());

    device.deviceProperties.isBlockedByPolicy = false;
    bluetoothConfig.updatePairedDevice(device);
    await flushTasks();
    assertNull(getManagedIcon());
  });

  test('True Wireless Images not shown when Fast pair disabled', async () => {
    loadTimeData.overrideValues({'enableFastPairFlag': false});
    init();
    bluetoothConfig.setBluetoothEnabledState(/*enabled=*/ true);

    const device = createDefaultBluetoothDevice(
        /*id=*/ '12345/6789&',
        /*publicName=*/ 'BeatsX',
        /*connectionState=*/
        DeviceConnectionState.kNotConnected,
        /*opt_nickname=*/ 'device1',
        /*opt_audioCapability=*/
        AudioOutputCapability.kCapableOfAudioOutput,
        /*opt_deviceType=*/ DeviceType.kMouse,
        /*opt_isBlockedByPolicy=*/ true);
    const fakeUrl = {url: 'fake_image'};

    device.deviceProperties.imageInfo = {
      trueWirelessImages: {
        leftBudImageUrl: fakeUrl,
        caseImageUrl: fakeUrl,
        rightBudImageUrl: fakeUrl,
      },
      defaultImageUrl: {url: ''},
    };
      device.deviceProperties.batteryInfo = {
        ...getDefaultDeviceBatteryInfo(),
      leftBudInfo: {batteryPercentage: 90},
    };

    bluetoothConfig.appendToPairedDeviceList([device]);
    await flushTasks();

    navigateToDeviceDetailPage('12345/6789&');

    // Since Fast Pair flag is false, we don't show the component.
    await flushTasks();
    assertNull(getTrueWirelessImages());
  });

  test('True Wireless Images shown when expected', async () => {
    loadTimeData.overrideValues({'enableFastPairFlag': true});
    init();
    bluetoothConfig.setBluetoothEnabledState(/*enabled=*/ true);

    const device = createDefaultBluetoothDevice(
        /*id=*/ '12345/6789&',
        /*publicName=*/ 'BeatsX',
        /*connectionState=*/
        DeviceConnectionState.kNotConnected,
        /*opt_nickname=*/ 'device1',
        /*opt_audioCapability=*/
        AudioOutputCapability.kCapableOfAudioOutput,
        /*opt_deviceType=*/ DeviceType.kMouse,
        /*opt_isBlockedByPolicy=*/ true);
    const fakeUrl = {url: 'fake_image'};
    // Emulate missing the right bud image.
    device.deviceProperties.imageInfo = {
      trueWirelessImages: {
        leftBudImageUrl: fakeUrl,
        rightBudImageUrl: {url: ''},
        caseImageUrl: fakeUrl,
      },
      defaultImageUrl: {url: ''},
    };
      device.deviceProperties.batteryInfo = {
        ...getDefaultDeviceBatteryInfo(),
      leftBudInfo: {batteryPercentage: 90},
    };

    bluetoothConfig.appendToPairedDeviceList([device]);
    await flushTasks();

    navigateToDeviceDetailPage('12345/6789&');

    // Don't display component unless either the default image is
    // present OR all of the true wireless images are present.
    await flushTasks();
    assertNull(getTrueWirelessImages());

    // Try again with all 3 True Wireless images.
    device.deviceProperties.imageInfo!.trueWirelessImages!.rightBudImageUrl =
        fakeUrl;
    bluetoothConfig.updatePairedDevice(device);
    await flushTasks();
    assertTrue(!!getTrueWirelessImages());

    // Try again with just default image.
    device.deviceProperties.imageInfo = {
      defaultImageUrl: fakeUrl,
      trueWirelessImages: undefined,
    };
    bluetoothConfig.updatePairedDevice(device);
    await flushTasks();
    assertTrue(!!getTrueWirelessImages());

    // If battery info is not available, only show True Wireless
    // component if not connected.
    device.deviceProperties.batteryInfo = {
        ...getDefaultDeviceBatteryInfo(),
    };
    device.deviceProperties.connectionState =
        DeviceConnectionState.kNotConnected;
    bluetoothConfig.updatePairedDevice(device);
    await flushTasks();
    assertTrue(!!getTrueWirelessImages());

    device.deviceProperties.connectionState = DeviceConnectionState.kConnecting;
    bluetoothConfig.updatePairedDevice(device);
    await flushTasks();
    assertTrue(!!getTrueWirelessImages());

    device.deviceProperties.connectionState = DeviceConnectionState.kConnected;
    bluetoothConfig.updatePairedDevice(device);
    await flushTasks();
    assertNull(getTrueWirelessImages());

    // Having either default battery info or True Wireless battery info
    // should show True Wireless component if device is connected.
    device.deviceProperties.batteryInfo = {
        ...getDefaultDeviceBatteryInfo(),
      defaultProperties: {batteryPercentage: 90},
    };
    bluetoothConfig.updatePairedDevice(device);
    await flushTasks();
    assertTrue(!!getTrueWirelessImages());

    device.deviceProperties.batteryInfo = {
        ...getDefaultDeviceBatteryInfo(),
      rightBudInfo: {batteryPercentage: 90},
    };
    bluetoothConfig.updatePairedDevice(device);
    await flushTasks();
    assertTrue(!!getTrueWirelessImages());
  });

  test(
      'Show change settings row, and navigate to subpages' +
      'w/ no per-device settings',
      async () => {
        loadTimeData.overrideValues({
          enableInputDeviceSettingsSplit: false,
        });

        init();
        bluetoothConfig.setBluetoothEnabledState(/*enabled=*/ true);

        const device1 = createDefaultBluetoothDevice(
            /*id=*/ '12//345&6789',
            /*publicName=*/ 'BeatsX',
            /*connectionState=*/
            DeviceConnectionState.kConnected,
            /*opt_nickname=*/ 'device1',
            /*opt_audioCapability=*/
            AudioOutputCapability.kCapableOfAudioOutput,
            /*opt_deviceType=*/ DeviceType.kMouse);

        bluetoothConfig.appendToPairedDeviceList([device1]);
        await flushTasks();

        navigateToDeviceDetailPage('12//345&6789');

        device1.deviceProperties.connectionState =
            DeviceConnectionState.kNotConnected;
        bluetoothConfig.updatePairedDevice(device1);
        await flushTasks();
        assertNull(getChangeMouseSettings());

        device1.deviceProperties.connectionState =
            DeviceConnectionState.kConnected;
        bluetoothConfig.updatePairedDevice(device1);
        await flushTasks();
        assertTrue(!!getChangeMouseSettings());

        getChangeMouseSettings()!.click();
        await flushTasks();

        assertEquals(routes.POINTERS, Router.getInstance().currentRoute);

        // Navigate back to the detail page.
        assertNotEquals(
            bluetoothDeviceDetailPage.shadowRoot!.activeElement,
            getChangeMouseSettings());
        let windowPopstatePromise = eventToPromise('popstate', window);
        Router.getInstance().navigateToPreviousRoute();
        await windowPopstatePromise;
        await waitAfterNextRender(bluetoothDeviceDetailPage);

        assertTrue(bluetoothDeviceDetailPage.getIsDeviceConnectedForTest());
        // Check that |#changeMouseSettings| has been focused.
        assertEquals(
            bluetoothDeviceDetailPage.shadowRoot!.activeElement,
            getChangeMouseSettings());

        device1.deviceProperties.deviceType = DeviceType.kKeyboard;
        bluetoothConfig.updatePairedDevice(device1);

        await flushTasks();
        assertNull(getChangeMouseSettings());
        assertTrue(!!getChangeKeyboardSettings());

        device1.deviceProperties.connectionState =
            DeviceConnectionState.kNotConnected;
        bluetoothConfig.updatePairedDevice(device1);
        await flushTasks();
        assertNull(getChangeKeyboardSettings());

        device1.deviceProperties.connectionState =
            DeviceConnectionState.kConnected;
        bluetoothConfig.updatePairedDevice(device1);
        await flushTasks();
        assertTrue(!!getChangeKeyboardSettings());

        getChangeKeyboardSettings()!.click();
        await flushTasks();

        assertEquals(routes.KEYBOARD, Router.getInstance().currentRoute);

        // Navigate back to the detail page.
        assertNotEquals(
            bluetoothDeviceDetailPage.shadowRoot!.activeElement,
            getChangeKeyboardSettings());
        windowPopstatePromise = eventToPromise('popstate', window);
        Router.getInstance().navigateToPreviousRoute();
        await windowPopstatePromise;
        await waitAfterNextRender(bluetoothDeviceDetailPage);

        // This is needed or other tests will fail.
        // TODO(gordonseto): Figure out how to remove this.
        getChangeKeyboardSettings()!.click();
        await flushTasks();
      });

  test('Show change settings row, and navigate to subpages', async () => {
    loadTimeData.overrideValues({
      enableInputDeviceSettingsSplit: true,
    });

    init();
    bluetoothConfig.setBluetoothEnabledState(/*enabled=*/ true);

    const device1 = createDefaultBluetoothDevice(
        /*id=*/ '12//345&6789',
        /*publicName=*/ 'BeatsX',
        /*connectionState=*/
        DeviceConnectionState.kConnected,
        /*opt_nickname=*/ 'device1',
        /*opt_audioCapability=*/
        AudioOutputCapability.kCapableOfAudioOutput,
        /*opt_deviceType=*/ DeviceType.kMouse);

    bluetoothConfig.appendToPairedDeviceList([device1]);
    await flushTasks();

    assertNull(getChangeMouseSettings());
    assertNull(getChangeKeyboardSettings());

    navigateToDeviceDetailPage('12//345&6789');

    await flushTasks();
    assertTrue(bluetoothDeviceDetailPage.getIsDeviceConnectedForTest());
    assertTrue(!!getChangeMouseSettings());
    assertNull(getChangeKeyboardSettings());
    assertEquals(
        bluetoothDeviceDetailPage.i18n(
            'bluetoothDeviceDetailChangeDeviceSettingsMouse'),
        getChangeMouseSettings()!.label);

    device1.deviceProperties.connectionState =
        DeviceConnectionState.kNotConnected;
    bluetoothConfig.updatePairedDevice(device1);
    await flushTasks();
    assertNull(getChangeMouseSettings());

    device1.deviceProperties.connectionState = DeviceConnectionState.kConnected;
    bluetoothConfig.updatePairedDevice(device1);
    await flushTasks();
    assertTrue(!!getChangeMouseSettings());

    getChangeMouseSettings()!.click();
    await flushTasks();

    assertEquals(routes.PER_DEVICE_MOUSE, Router.getInstance().currentRoute);

    // Navigate back to the detail page.
    assertNotEquals(
        bluetoothDeviceDetailPage.shadowRoot!.activeElement,
        getChangeMouseSettings());
    let windowPopstatePromise = eventToPromise('popstate', window);
    Router.getInstance().navigateToPreviousRoute();
    await windowPopstatePromise;
    await waitAfterNextRender(bluetoothDeviceDetailPage);

    assertTrue(bluetoothDeviceDetailPage.getIsDeviceConnectedForTest());
    // Check that |#changeMouseSettings| has been focused.
    assertEquals(
        bluetoothDeviceDetailPage.shadowRoot!.activeElement,
        getChangeMouseSettings());

    device1.deviceProperties.deviceType = DeviceType.kKeyboard;
    bluetoothConfig.updatePairedDevice(device1);

    await flushTasks();
    assertNull(getChangeMouseSettings());
    assertTrue(!!getChangeKeyboardSettings());

    device1.deviceProperties.connectionState =
        DeviceConnectionState.kNotConnected;
    bluetoothConfig.updatePairedDevice(device1);
    await flushTasks();
    assertNull(getChangeKeyboardSettings());

    device1.deviceProperties.connectionState = DeviceConnectionState.kConnected;
    bluetoothConfig.updatePairedDevice(device1);
    await flushTasks();
    assertTrue(!!getChangeKeyboardSettings());

    getChangeKeyboardSettings()!.click();
    await flushTasks();

    assertEquals(routes.PER_DEVICE_KEYBOARD, Router.getInstance().currentRoute);

    // Navigate back to the detail page.
    assertNotEquals(
        bluetoothDeviceDetailPage.shadowRoot!.activeElement,
        getChangeKeyboardSettings());
    windowPopstatePromise = eventToPromise('popstate', window);
    Router.getInstance().navigateToPreviousRoute();
    await windowPopstatePromise;
    await waitAfterNextRender(bluetoothDeviceDetailPage);

    assertTrue(bluetoothDeviceDetailPage.getIsDeviceConnectedForTest());
    // Check that |#changeKeyboardSettings| has been focused.
    assertEquals(
        bluetoothDeviceDetailPage.shadowRoot!.activeElement,
        getChangeKeyboardSettings());

    device1.deviceProperties.deviceType = DeviceType.kKeyboardMouseCombo;
    bluetoothConfig.updatePairedDevice(device1);

    await flushTasks();
    assertTrue(!!getChangeMouseSettings());
    assertTrue(!!getChangeKeyboardSettings());

    // This is needed or other tests will fail.
    // TODO(gordonseto): Figure out how to remove this.
    getChangeKeyboardSettings()!.click();
    await flushTasks();
  });

  test('Device becomes unavailable while viewing pages.', async () => {
    init();
    bluetoothConfig.setBluetoothEnabledState(/*enabled=*/ true);

    const windowPopstatePromise = eventToPromise('popstate', window);

    const device1 = createDefaultBluetoothDevice(
        /*id=*/ '12345/6789&',
        /*publicName=*/ 'BeatsX',
        /*connectionState=*/
        DeviceConnectionState.kConnected,
        /*opt_nickname=*/ 'device1',
        /*opt_audioCapability=*/
        AudioOutputCapability.kCapableOfAudioOutput,
        /*opt_deviceType=*/ DeviceType.kMouse);

    const device2 = createDefaultBluetoothDevice(
        /*id=*/ '987654321',
        /*publicName=*/ 'MX 3',
        /*connectionState=*/
        DeviceConnectionState.kConnected,
        /*opt_nickname=*/ 'device2',
        /*opt_audioCapability=*/
        AudioOutputCapability.kCapableOfAudioOutput,
        /*opt_deviceType=*/ DeviceType.kMouse);

    bluetoothConfig.appendToPairedDeviceList([device1, device2]);
    await flushTasks();

    const params = new URLSearchParams();
    params.append('id', '12345/6789&');
    Router.getInstance().navigateTo(routes.BLUETOOTH_DEVICE_DETAIL, params);
    await flushTasks();
    assertEquals(
        'device1',
        (bluetoothDeviceDetailPage.parentNode as OsSettingsSubpageElement)
            .pageTitle);
    assertTrue(!!bluetoothDeviceDetailPage.getDeviceIdForTest());

    // Device becomes unavailable in the devices list subpage. We should still
    // have a device id present since the device id would not be reset to an
    // empty string.
    Router.getInstance().navigateTo(routes.BLUETOOTH_DEVICES, params);
    await flushTasks();
    bluetoothConfig.removePairedDevice(device1);
    await flushTasks();
    assertTrue(!!bluetoothDeviceDetailPage.getDeviceIdForTest());

    // Add device back and check for when device becomes unavailable in
    // the device detail subpage.
    bluetoothConfig.appendToPairedDeviceList([device1]);
    Router.getInstance().navigateTo(routes.BLUETOOTH_DEVICE_DETAIL, params);
    bluetoothConfig.removePairedDevice(device1);

    // Device id is removed and navigation backward should occur.
    await windowPopstatePromise;
    assertEquals('', bluetoothDeviceDetailPage.getDeviceIdForTest());
  });

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

    const getDeviceTypeIcon = () => {
      return bluetoothDeviceDetailPage.shadowRoot!.querySelector(
          'bluetooth-icon');
    };

    const getBluetoothDeviceNameLabel = () => {
      const label = bluetoothDeviceDetailPage.shadowRoot!.querySelector(
          '#bluetoothDeviceNameLabel');
      assertTrue(!!label);
      return label;
    };

    const getBluetoothDeviceBatteryInfo = () =>
        bluetoothDeviceDetailPage.shadowRoot!.querySelector('#batteryInfo');

    const getNonAudioOutputDeviceMessage = () =>
        bluetoothDeviceDetailPage.shadowRoot!.querySelector(
            '#nonAudioOutputDeviceMessage');

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

    assertTrue(!!getDeviceTypeIcon());
    assertTrue(!!getBluetoothStateText());
    assertTrue(!!getBluetoothDeviceNameLabel());
    assertNull(getBluetoothForgetBtn());
    assertNull(getBluetoothConnectDisconnectBtn());
    assertNull(getBluetoothDeviceBatteryInfo());
    assertNull(getNonAudioOutputDeviceMessage());

    const deviceNickname = 'device1';
    const device1 = createDefaultBluetoothDevice(
        /*id=*/ '123456789',
        /*publicName=*/ 'BeatsX',
        /*connectionState=*/
        DeviceConnectionState.kConnected, deviceNickname,
        /*opt_udioCapability=*/
        AudioOutputCapability.kCapableOfAudioOutput,
        /*opt_deviceType=*/ DeviceType.kHeadset);

    device1.deviceProperties.batteryInfo = {
        ...getDefaultDeviceBatteryInfo(),
      defaultProperties: {batteryPercentage: 90},
    };
    bluetoothConfig.appendToPairedDeviceList([device1]);
    await flushTasks();

    navigateToDeviceDetailPage('123456789');
    await flushTasks();

    assertTrue(!!getBluetoothForgetBtn());

    // Simulate connected state and audio capable.
    assertTrue(!!getBluetoothConnectDisconnectBtn());
    assertEquals(
        bluetoothDeviceDetailPage.i18n('bluetoothDeviceDetailConnected'),
        getBluetoothStateText().textContent!.trim());
    assertTrue(bluetoothDeviceDetailPage.getIsDeviceConnectedForTest());
    assertEquals(
        deviceNickname, getBluetoothDeviceNameLabel().textContent!.trim());
    assertTrue(!!getBluetoothDeviceBatteryInfo());
    assertNull(getNonAudioOutputDeviceMessage());

    // Simulate disconnected state and not audio capable.
    device1.deviceProperties.connectionState =
        DeviceConnectionState.kNotConnected;
    device1.deviceProperties.audioCapability =
        AudioOutputCapability.kNotCapableOfAudioOutput;
    device1.deviceProperties.batteryInfo = {
        ...getDefaultDeviceBatteryInfo(),
    };
    bluetoothConfig.updatePairedDevice(device1);
    await flushTasks();

    assertNull(getBluetoothConnectDisconnectBtn());
    assertEquals(
        bluetoothDeviceDetailPage.i18n('bluetoothDeviceDetailDisconnected'),
        getBluetoothStateText().textContent!.trim());
    assertFalse(bluetoothDeviceDetailPage.getIsDeviceConnectedForTest());
    assertNull(getBluetoothDeviceBatteryInfo());
    assertEquals(
        bluetoothDeviceDetailPage.i18n(
            'bluetoothDeviceDetailForgetA11yLabel', deviceNickname),
        getBluetoothForgetBtn()!.ariaLabel);
    assertTrue(!!getNonAudioOutputDeviceMessage());
    assertEquals(
        bluetoothDeviceDetailPage.i18n(
            'bluetoothDeviceDetailHIDMessageDisconnected'),
        getNonAudioOutputDeviceMessage()!.textContent!.trim());

    // Simulate connected state and not audio capable.
    device1.deviceProperties.connectionState = DeviceConnectionState.kConnected;
    bluetoothConfig.updatePairedDevice(device1);
    await flushTasks();

    assertTrue(!!getNonAudioOutputDeviceMessage());
    assertEquals(
        bluetoothDeviceDetailPage.i18n(
            'bluetoothDeviceDetailHIDMessageConnected'),
        getNonAudioOutputDeviceMessage()!.textContent!.trim());

    device1.deviceProperties.audioCapability =
        AudioOutputCapability.kCapableOfAudioOutput;
    bluetoothConfig.updatePairedDevice(device1);
    // Navigate away from details subpage with while connected and navigate
    // back.
    const windowPopstatePromise = eventToPromise('popstate', window);
    Router.getInstance().navigateToPreviousRoute();
    await windowPopstatePromise;

    navigateToDeviceDetailPage('123456789');
    await flushTasks();

    assertTrue(!!getBluetoothConnectDisconnectBtn());
    assertEquals(
        bluetoothDeviceDetailPage.i18n('bluetoothDeviceDetailConnected'),
        getBluetoothStateText().textContent!.trim());
  });

  test(
      'Change device dialog is shown after change name button click',
      async () => {
        init();
        bluetoothConfig.setBluetoothEnabledState(/*enabled=*/ true);

        const getChangeDeviceNameDialog = () =>
            bluetoothDeviceDetailPage.shadowRoot!.querySelector(
                '#changeDeviceNameDialog');

        const device1 = createDefaultBluetoothDevice(
            /*id=*/ '12//345&6789',
            /*publicName=*/ 'BeatsX',
            /*connectionState=*/
            DeviceConnectionState.kConnected,
            /*opt_nickname=*/ 'device1',
            /*opt_audioCapability=*/
            AudioOutputCapability.kCapableOfAudioOutput,
            /*opt_deviceType=*/ DeviceType.kMouse);

        bluetoothConfig.appendToPairedDeviceList([device1]);
        await flushTasks();

        navigateToDeviceDetailPage('12//345&6789');
        await flushTasks();

        assertNull(getChangeDeviceNameDialog());

        const changeNameBtn =
            bluetoothDeviceDetailPage.shadowRoot!
                .querySelector<HTMLButtonElement>('#changeNameBtn');
        assertTrue(!!changeNameBtn);
        changeNameBtn.click();

        await flushTasks();
        assertTrue(!!getChangeDeviceNameDialog());
      });

  test('Landing on page while device is still connecting', async () => {
    init();
    bluetoothConfig.setBluetoothEnabledState(/*enabled=*/ true);

    const id = '12//345&6789';
    const device1 = createDefaultBluetoothDevice(
        /*id=*/ id,
        /*publicName=*/ 'BeatsX',
        /*connectionState=*/
        DeviceConnectionState.kConnecting,
        /*opt_nickname=*/ 'device1',
        /*opt_audioCapability=*/
        AudioOutputCapability.kCapableOfAudioOutput,
        /*opt_deviceType=*/ DeviceType.kMouse);

    bluetoothConfig.appendToPairedDeviceList([device1]);
    await flushTasks();

    navigateToDeviceDetailPage(id);
    await flushTasks();
    assertTrue(!!getBluetoothConnectDisconnectBtn());
    assertEquals(
        bluetoothDeviceDetailPage.i18n('bluetoothConnecting'),
        getBluetoothStateText().textContent!.trim());
    assertNull(getConnectionFailedText());
    assertTrue(getBluetoothConnectDisconnectBtn()!.disabled);
    assertEquals(
        bluetoothDeviceDetailPage.i18n('bluetoothConnect'),
        getBluetoothConnectDisconnectBtn()!.textContent!.trim());
  });

  test('Connect/Disconnect/forget states and error message', async () => {
    loadTimeData.overrideValues({'enableFastPairFlag': true});
    init();
    bluetoothConfig.setBluetoothEnabledState(/*enabled=*/ true);

    const windowPopstatePromise = eventToPromise('popstate', window);

    const getBluetoothDialogForgetButton = () => {
      const forgetDeviceDialog =
          bluetoothDeviceDetailPage.shadowRoot!.querySelector(
              '#forgetDeviceDialog');
      assertTrue(!!forgetDeviceDialog);
      const button =
          forgetDeviceDialog.shadowRoot!.querySelector<HTMLButtonElement>(
              '#forget');
      assertTrue(!!button);
      return button;
    };

    const assertUIState =
        (isShowingConnectionFailed: boolean,
         isConnectDisconnectBtnDisabled: boolean, bluetoothStateText: string,
         connectDisconnectBtnText: string) => {
          assertEquals(isShowingConnectionFailed, !!getConnectionFailedText());
          assertEquals(
              isConnectDisconnectBtnDisabled,
              getBluetoothConnectDisconnectBtn()!.disabled);
          assertEquals(
              bluetoothStateText, getBluetoothStateText().textContent!.trim());
          assertEquals(
              connectDisconnectBtnText,
              getBluetoothConnectDisconnectBtn()!.textContent!.trim());
        };

    const id = '12345/6789&';
    const device1 = createDefaultBluetoothDevice(
        id,
        /*publicName=*/ 'BeatsX',
        /*connectionState=*/
        DeviceConnectionState.kNotConnected,
        /*opt_nickname=*/ 'device1',
        /*opt_audioCapability=*/
        AudioOutputCapability.kCapableOfAudioOutput,
        /*opt_deviceType=*/ DeviceType.kMouse);

    device1.deviceProperties.batteryInfo = {
        ...getDefaultDeviceBatteryInfo(),
      defaultProperties: {batteryPercentage: 90},
    };

    bluetoothConfig.appendToPairedDeviceList([device1]);
    await flushTasks();

    navigateToDeviceDetailPage(id);

    await flushTasks();
    assertTrue(!!getBluetoothConnectDisconnectBtn());
    // Disconnected without error.
    assertUIState(
        /*isShowingConnectionFailed=*/ false,
        /*isConnectDisconnectBtnDisabled=*/ false,
        /*bluetoothStateText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothDeviceDetailDisconnected'),
        /*connectDisconnectBtnText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothConnect'));

    // Try to connect.
    getBluetoothConnectDisconnectBtn()!.click();
    await flushTasks();
    // Connecting.
    assertUIState(
        /*isShowingConnectionFailed=*/ false,
        /*isConnectDisconnectBtnDisabled=*/ true,
        /*bluetoothStateText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothConnecting'),
        /*connectDisconnectBtnText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothConnect'));
    bluetoothConfig.completeConnect(/*success=*/ false);

    // Connection fails.
    await flushTasks();
    // Disconnected with error.
    assertUIState(
        /*isShowingConnectionFailed=*/ true,
        /*isConnectDisconnectBtnDisabled=*/ false,
        /*bluetoothStateText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothDeviceDetailDisconnected'),
        /*connectDisconnectBtnText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothConnect'));

    // Change device while connection failed text is shown.
    bluetoothConfig.appendToPairedDeviceList([Object.assign({}, device1)]);
    await flushTasks();

    // Disconnected with error.
    assertUIState(
        /*isShowingConnectionFailed=*/ true,
        /*isConnectDisconnectBtnDisabled=*/ false,
        /*bluetoothStateText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothDeviceDetailDisconnected'),
        /*connectDisconnectBtnText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothConnect'));

    // Try to connect with success.
    getBluetoothConnectDisconnectBtn()!.click();
    await flushTasks();
    bluetoothConfig.completeConnect(/*success=*/ true);
    // Connection success.
    assertUIState(
        /*isShowingConnectionFailed=*/ false,
        /*isConnectDisconnectBtnDisabled=*/ false,
        /*bluetoothStateText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothDeviceDetailConnected'),
        /*connectDisconnectBtnText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothDisconnect'));

    // Disconnect device and check that connection error is not shown.
    getBluetoothConnectDisconnectBtn()!.click();

    // Disconnecting.
    assertUIState(
        /*isShowingConnectionFailed=*/ false,
        /*isConnectDisconnectBtnDisabled=*/ true,
        /*bluetoothStateText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothDeviceDetailConnected'),
        /*connectDisconnectBtnText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothDisconnect'));
    await flushTasks();
    bluetoothConfig.completeDisconnect(/*success=*/ true);
    await flushTasks();
    // Disconnected without error.
    assertUIState(
        /*isShowingConnectionFailed=*/ false,
        /*isConnectDisconnectBtnDisabled=*/ false,
        /*bluetoothStateText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothDeviceDetailDisconnected'),
        /*connectDisconnectBtnText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothConnect'));

    // Try to connect with error.
    getBluetoothConnectDisconnectBtn()!.click();
    await flushTasks();
    bluetoothConfig.completeConnect(/*success=*/ false);
    await flushTasks();
    // Disconnected with error.
    assertUIState(
        /*isShowingConnectionFailed=*/ true,
        /*isConnectDisconnectBtnDisabled=*/ false,
        /*bluetoothStateText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothDeviceDetailDisconnected'),
        /*connectDisconnectBtnText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothConnect'));

    // Device automatically reconnects without calling connect. This
    // can happen if connection failure was because device was turned off
    // and is turned on. We expect connection error text to not show when
    // disconnected.
    device1.deviceProperties.connectionState = DeviceConnectionState.kConnected;
    bluetoothConfig.updatePairedDevice(device1);
    await flushTasks();
    // Connection success.
    assertUIState(
        /*isShowingConnectionFailed=*/ false,
        /*isConnectDisconnectBtnDisabled=*/ false,
        /*bluetoothStateText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothDeviceDetailConnected'),
        /*connectDisconnectBtnText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothDisconnect'));

    // Disconnect device and check that connection error is not shown.
    getBluetoothConnectDisconnectBtn()!.click();
    await flushTasks();
    bluetoothConfig.completeDisconnect(/*success=*/ true);
    await flushTasks();
    // Disconnected without error.
    assertUIState(
        /*isShowingConnectionFailed=*/ false,
        /*isConnectDisconnectBtnDisabled=*/ false,
        /*bluetoothStateText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothDeviceDetailDisconnected'),
        /*connectDisconnectBtnText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothConnect'));

    // Retry connection with success.
    getBluetoothConnectDisconnectBtn()!.click();
    await flushTasks();
    // Connecting.
    assertUIState(
        /*isShowingConnectionFailed=*/ false,
        /*isConnectDisconnectBtnDisabled=*/ true,
        /*bluetoothStateText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothConnecting'),
        /*connectDisconnectBtnText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothConnect'));
    bluetoothConfig.completeConnect(/*success=*/ true);
    await flushTasks();
    // Connection success.
    assertUIState(
        /*isShowingConnectionFailed=*/ false,
        /*isConnectDisconnectBtnDisabled=*/ false,
        /*bluetoothStateText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothDeviceDetailConnected'),
        /*connectDisconnectBtnText=*/
        bluetoothDeviceDetailPage.i18n('bluetoothDisconnect'));

    const forgetDialogOpen =
        eventToPromise('cr-dialog-open', bluetoothDeviceDetailPage);

    // Forget device.
    getBluetoothForgetBtn()!.click();
    await flushTasks();
    await forgetDialogOpen;
    getBluetoothDialogForgetButton().click();

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

    // Device and device Id should be null after navigating backward.
    assertNull(bluetoothDeviceDetailPage.getDeviceForTest());
    assertEquals('', bluetoothDeviceDetailPage.getDeviceIdForTest());
  });

  test('Route to device details page', () => {
    init();
    assertEquals(0, browserProxy.getShowBluetoothRevampHatsSurveyCount());
    navigateToDeviceDetailPage('id');
    assertEquals(
        1, browserProxy.getShowBluetoothRevampHatsSurveyCount(),
        'Count failed to increase');
  });
});