chromium/chrome/test/data/webui/bluetooth_internals/bluetooth_internals_test.js

// 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 {adapterBroker, checkSystemPermissions, devices, initializeViews, pageManager, sidebarObj} from 'chrome://bluetooth-internals/bluetooth_internals.js';
import {BluetoothInternalsHandler} from 'chrome://bluetooth-internals/bluetooth_internals.mojom-webui.js';
import {connectedDevices} from 'chrome://bluetooth-internals/device_broker.js';
import {dismissSnackbar, getSnackbarStateForTest, showSnackbar} from 'chrome://bluetooth-internals/snackbar.js';
import {ValueDataType} from 'chrome://bluetooth-internals/value_control.js';
import {assert, assertNotReached} from 'chrome://resources/js/assert.js';
import {PromiseResolver} from 'chrome://resources/js/promise_resolver.js';
import {$} from 'chrome://resources/js/util.js';
import {assertDeepEquals, assertEquals, assertFalse, assertThrows, assertTrue} from 'chrome://webui-test/chai_assert.js';
// <if expr="chromeos_ash">
import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
// </if>

import {fakeAdapterInfo, fakeCharacteristicInfo1, fakeDeviceInfo1, fakeDeviceInfo2, fakeDeviceInfo3, fakeServiceInfo1, fakeServiceInfo2, TestAdapter, TestBluetoothInternalsHandler, TestDevice} from './test_utils.js';

suite('bluetooth_internals', function() {
  const whenSetupDone = new PromiseResolver();
  let internalsHandler = null;
  let adapterFieldSet = null;
  let deviceTable = null;
  let sidebarNode = null;
  let bluetoothInternalsHandlerRemote = null;
  const pageNames = ['adapter', 'devices'];
  const EXPECTED_DEVICES = 2;

  suiteSetup(async function() {
    const internalsHandlerInterceptor =
        new MojoInterfaceInterceptor(BluetoothInternalsHandler.$interfaceName);
    internalsHandlerInterceptor.oninterfacerequest = (e) => {
      internalsHandler = new TestBluetoothInternalsHandler(e.handle);

      const testAdapter = new TestAdapter(fakeAdapterInfo());
      testAdapter.setTestDevices([
        fakeDeviceInfo1(),
        fakeDeviceInfo2(),
      ]);

      const testServices = [fakeServiceInfo1(), fakeServiceInfo2()];

      testAdapter.setTestServicesForTestDevice(
          fakeDeviceInfo1(), Object.assign({}, testServices));
      testAdapter.setTestServicesForTestDevice(
          fakeDeviceInfo2(), Object.assign({}, testServices));

      internalsHandler.setAdapterForTesting(testAdapter);
      whenSetupDone.resolve();
    };
    internalsHandlerInterceptor.start();
    bluetoothInternalsHandlerRemote = BluetoothInternalsHandler.getRemote();
    await checkSystemPermissions(
        bluetoothInternalsHandlerRemote, initializeViews);
    await whenSetupDone.promise;
    await Promise.all([
      internalsHandler.whenCalled('checkSystemPermissions'),
      internalsHandler.whenCalled('getAdapter'),
      internalsHandler.adapter.whenCalled('getInfo'),
      internalsHandler.adapter.whenCalled('getDevices'),
      internalsHandler.adapter.whenCalled('addObserver'),
    ]);
  });

  setup(function() {
    adapterFieldSet = document.querySelector('#adapter object-field-set');
    deviceTable = document.querySelector('#devices device-table')
                      .shadowRoot.querySelector('table');
    sidebarNode = document.querySelector('#sidebar');
    devices.resetForTest();
    adapterBroker.deviceAdded(fakeDeviceInfo1());
    adapterBroker.deviceAdded(fakeDeviceInfo2());
  });

  teardown(function() {
    internalsHandler.reset();
    sidebarObj.close();
    dismissSnackbar(true);
    connectedDevices.clear();

    pageManager.registeredPages.get('adapter').setAdapterInfo(
        fakeAdapterInfo());

    for (const pageName of pageManager.registeredPages.keys()) {
      const page = pageManager.registeredPages.get(pageName);

      if (pageNames.indexOf(pageName) < 0) {
        page.pageDiv.parentNode.removeChild(page.pageDiv);
        pageManager.unregister(page);
      }
    }

    // Close all of the dialogs.
    document.getElementById('need-location-services-on').close();
    document.getElementById('need-location-permission-and-services-on').close();
    document.getElementById('need-nearby-devices-permission').close();
    document.getElementById('need-location-permission').close();
    document.getElementById('can-not-request-permissions').close();
    document.getElementById('refresh-page').close();
  });

  /**
   * Updates device info and verifies the contents of the device table.
   * @param {!DeviceInfo} deviceInfo
   */
  function changeDevice(deviceInfo) {
    const deviceRow = deviceTable.querySelector(
        '#' + escapeDeviceAddress(deviceInfo.address));
    const nameForDisplayColumn = deviceRow.children[0];
    const addressColumn = deviceRow.children[1];
    const rssiColumn = deviceRow.children[2];
    const serviceUuidsColumn = deviceRow.children[3];
    const manufacturerDataColumn = deviceRow.children[4];

    assertTrue(!!nameForDisplayColumn);
    assertTrue(!!addressColumn);
    assertTrue(!!rssiColumn);
    assertTrue(!!serviceUuidsColumn);
    assertTrue(!!manufacturerDataColumn);

    adapterBroker.deviceChanged(deviceInfo);

    assertEquals(deviceInfo.nameForDisplay, nameForDisplayColumn.textContent);
    assertEquals(deviceInfo.address, addressColumn.textContent);

    if (deviceInfo.rssi) {
      assertEquals(String(deviceInfo.rssi.value), rssiColumn.textContent);
    }

    if (deviceInfo.serviceUuids) {
      assertEquals(
          formatServiceUuids(deviceInfo.serviceUuids),
          serviceUuidsColumn.textContent);
    }

    if (deviceInfo.manufacturerDataMap) {
      assertEquals(
          formatManufacturerDataMap(deviceInfo.manufacturerDataMap),
          manufacturerDataColumn.textContent);
    }
  }

  /**
   * Format in a user readable way service UUIDs.
   * @param ?Array<UUID> uuids
   * @return {string}
   */
  function formatServiceUuids(serviceUuids) {
    if (!serviceUuids) {
      return '';
    }
    return serviceUuids.map(service => service.uuid).join(', ');
  }

  /**
   * Format in a user readable way device manufacturer data map. Keys are
   * Bluetooth company identifiers (unsigned short), values are bytes.
   * @param {Map<string, array<number>>} manufacturerDataMap
   * @return {string}
   */
  function formatManufacturerDataMap(manufacturerDataMap) {
    return Object.entries(manufacturerDataMap)
        .map(([key, value]) => {
          const companyIdentifier = parseInt(key).toString(16).padStart(4, '0');
          const data = value.map(v => v.toString(16).padStart(2, '0')).join('');
          return `0x${companyIdentifier} 0x${data}`;
        })
        .join(' | ');
  }

  /**
   * Escapes colons in a device address for CSS formatting.
   * @param {string} address
   */
  function escapeDeviceAddress(address) {
    return address.replace(/:/g, '\\:');
  }

  /**
   * Expects whether device with |address| is removed.
   * @param {string} address
   * @param {boolean} expectRemoved
   */
  function expectDeviceRemoved(address, expectRemoved) {
    const removedRow =
        deviceTable.querySelector('#' + escapeDeviceAddress(address));

    assertEquals(expectRemoved, removedRow.classList.contains('removed'));
  }

  /**
   * Tests whether a device is added successfully and not duplicated.
   */
  test('DeviceAdded', function() {
    let devices = deviceTable.querySelectorAll('tbody tr');
    assertEquals(EXPECTED_DEVICES, devices.length);

    // Copy device info because device collection will not copy this object.
    const infoCopy = fakeDeviceInfo3();
    adapterBroker.deviceAdded(infoCopy);

    // Same device shouldn't appear twice.
    adapterBroker.deviceAdded(infoCopy);

    devices = deviceTable.querySelectorAll('tbody tr');
    assertEquals(EXPECTED_DEVICES + 1, devices.length);
  });

  /**
   * Tests whether a device is marked properly as removed.
   */
  test('DeviceSetToRemoved', function() {
    let devices = deviceTable.querySelectorAll('tbody tr');
    assertEquals(EXPECTED_DEVICES, devices.length);

    const fakeDevice = fakeDeviceInfo2();
    adapterBroker.deviceRemoved(fakeDevice);

    // The number of rows shouldn't change.
    devices = deviceTable.querySelectorAll('tbody tr');
    assertEquals(EXPECTED_DEVICES, devices.length);

    expectDeviceRemoved(fakeDevice.address, true);
  });

  /**
   * Tests whether a changed device updates the device table properly.
   */
  test('DeviceChanged', function() {
    const devices = deviceTable.querySelectorAll('tbody tr');
    assertEquals(EXPECTED_DEVICES, devices.length);

    // Copy device info because device collection will not copy this object.
    const newDeviceInfo = fakeDeviceInfo1();
    newDeviceInfo.nameForDisplay = 'DDDD';
    newDeviceInfo.rssi = {value: -20};
    newDeviceInfo.serviceUuids = [
      {uuid: '00002a05-0000-1000-8000-00805f9b34fb'},
      {uuid: '0000180d-0000-1000-8000-00805f9b34fb'},
    ];

    changeDevice(newDeviceInfo);
  });

  /**
   * Tests the entire device cycle, added -> updated -> removed -> re-added.
   */
  test('DeviceUpdateCycle', function() {
    const devices = deviceTable.querySelectorAll('tbody tr');
    assertEquals(EXPECTED_DEVICES, devices.length);

    // Copy device info because device collection will not copy this object.
    const originalDeviceInfo = fakeDeviceInfo3();
    adapterBroker.deviceAdded(originalDeviceInfo);

    const newDeviceInfo = fakeDeviceInfo3();
    newDeviceInfo.nameForDisplay = 'DDDD';
    newDeviceInfo.rssi = {value: -20};
    newDeviceInfo.serviceUuids = [
      {uuid: '00002a05-0000-1000-8000-00805f9b34fb'},
      {uuid: '0000180d-0000-1000-8000-00805f9b34fb'},
    ];

    changeDevice(newDeviceInfo);
    changeDevice(originalDeviceInfo);

    adapterBroker.deviceRemoved(originalDeviceInfo);
    expectDeviceRemoved(originalDeviceInfo.address, true);

    adapterBroker.deviceAdded(originalDeviceInfo);
    expectDeviceRemoved(originalDeviceInfo.address, false);
  });

  test('DeviceAddedRssiCheck', function() {
    const devices = deviceTable.querySelectorAll('tbody tr');
    assertEquals(EXPECTED_DEVICES, devices.length);

    // Copy device info because device collection will not copy this object.
    const newDeviceInfo = fakeDeviceInfo3();
    adapterBroker.deviceAdded(newDeviceInfo);

    const deviceRow = deviceTable.querySelector(
        '#' + escapeDeviceAddress(newDeviceInfo.address));
    const rssiColumn = deviceRow.children[2];
    assertEquals('Unknown', rssiColumn.textContent);

    const newDeviceInfo1 = fakeDeviceInfo3();
    newDeviceInfo1.rssi = {value: -42};
    adapterBroker.deviceChanged(newDeviceInfo1);
    assertEquals('-42', rssiColumn.textContent);

    // Device table should keep last valid rssi value.
    const newDeviceInfo2 = fakeDeviceInfo3();
    newDeviceInfo2.rssi = null;
    adapterBroker.deviceChanged(newDeviceInfo2);
    assertEquals('-42', rssiColumn.textContent);

    const newDeviceInfo3 = fakeDeviceInfo3();
    newDeviceInfo3.rssi = {value: -17};
    adapterBroker.deviceChanged(newDeviceInfo3);
    assertEquals('-17', rssiColumn.textContent);
  });

  /* Sidebar Tests */
  test('Sidebar_Setup', function() {
    const sidebarItems =
        Array.from(sidebarNode.querySelectorAll('.sidebar-content li'));

    pageNames.forEach(function(pageName) {
      assertTrue(sidebarItems.some(function(item) {
        return item.dataset.pageName === pageName;
      }));
    });
  });

  test('Sidebar_DefaultState', function() {
    // Sidebar should be closed by default.
    assertFalse(sidebarNode.classList.contains('open'));
  });

  test('Sidebar_OpenClose', function() {
    sidebarObj.open();
    assertTrue(sidebarNode.classList.contains('open'));
    sidebarObj.close();
    assertFalse(sidebarNode.classList.contains('open'));
  });

  test('Sidebar_OpenTwice', function() {
    // Multiple calls to open shouldn't change the state.
    sidebarObj.open();
    sidebarObj.open();
    assertTrue(sidebarNode.classList.contains('open'));
  });

  test('Sidebar_CloseTwice', function() {
    // Multiple calls to close shouldn't change the state.
    sidebarObj.close();
    sidebarObj.close();
    assertFalse(sidebarNode.classList.contains('open'));
  });

  /* Snackbar Tests */

  /**
   * Checks snackbar showing status and returns a Promise that resolves when
   * |pendingSnackbar| is shown. If the snackbar is already showing, the
   * Promise resolves immediately.
   * @param {!Snackbar} pendingSnackbar
   * @return {!Promise}
   */
  function whenSnackbarShows(pendingSnackbar) {
    return new Promise(function(resolve) {
      if (pendingSnackbar.classList.contains('open')) {
        resolve();
      } else {
        pendingSnackbar.addEventListener('showed', resolve);
      }
    });
  }

  /**
   * Performs final checks for snackbar tests.
   * @return {!Promise} Promise is fulfilled when the checks finish.
   */
  function finishSnackbarTest() {
    return new Promise(function(resolve) {
      // Let event queue finish.
      setTimeout(function() {
        assertEquals(0, $('snackbar-container').children.length);
        assertFalse(!!getSnackbarStateForTest().current);
        resolve();
      }, 50);
    });
  }

  test('Snackbar_ShowTimeout', function(done) {
    const snackbar1 = showSnackbar('Message 1');
    assertEquals(1, $('snackbar-container').children.length);

    snackbar1.addEventListener('dismissed', function() {
      finishSnackbarTest().then(done);
    });
  });

  test('Snackbar_ShowDismiss', function() {
    const snackbar1 = showSnackbar('Message 1');
    assertEquals(1, $('snackbar-container').children.length);

    return whenSnackbarShows(snackbar1)
        .then(function() {
          return dismissSnackbar();
        })
        .then(finishSnackbarTest);
  });

  test('Snackbar_QueueThreeDismiss', function() {
    const expectedCalls = 3;
    let actualCalls = 0;

    const snackbar1 = showSnackbar('Message 1');
    const snackbar2 = showSnackbar('Message 2');
    const snackbar3 = showSnackbar('Message 3');

    assertEquals(1, $('snackbar-container').children.length);
    assertEquals(2, getSnackbarStateForTest().numPending);

    function next() {
      actualCalls++;
      return dismissSnackbar();
    }

    whenSnackbarShows(snackbar1).then(next);
    whenSnackbarShows(snackbar2).then(next);
    return whenSnackbarShows(snackbar3)
        .then(next)
        .then(function() {
          assertEquals(expectedCalls, actualCalls);
        })
        .then(finishSnackbarTest);
  });

  test('Snackbar_QueueThreeDismissAll', function() {
    const expectedCalls = 1;
    const actualCalls = 0;

    const snackbar1 = showSnackbar('Message 1');
    const snackbar2 = showSnackbar('Message 2');
    const snackbar3 = showSnackbar('Message 3');

    assertEquals(1, $('snackbar-container').children.length);
    assertEquals(2, getSnackbarStateForTest().numPending);

    function next() {
      assertTrue(false);
    }

    whenSnackbarShows(snackbar2).then(next);
    snackbar2.addEventListener('dismissed', next);
    whenSnackbarShows(snackbar3).then(next);
    snackbar3.addEventListener('dismissed', next);

    return whenSnackbarShows(snackbar1)
        .then(function() {
          return dismissSnackbar(true);
        })
        .then(function() {
          assertEquals(0, getSnackbarStateForTest().numPending);
          assertFalse(!!getSnackbarStateForTest().current);
        })
        .then(finishSnackbarTest);
  });

  /* AdapterPage Tests */
  function checkAdapterFieldSet(adapterInfo) {
    for (const propName in adapterInfo) {
      const valueCell = adapterFieldSet.shadowRoot.querySelector(
          `fieldset [data-field="${propName}"]`);
      const value = adapterInfo[propName];

      if (typeof (value) === 'boolean') {
        assertEquals(value, valueCell.classList.contains('checked'));
      } else if (typeof (value) === 'string') {
        assertEquals(value, valueCell.textContent);
      } else {
        assertNotReached(
            'boolean or string type expected but got ' + typeof (value));
      }
    }
  }

  test('AdapterPage_DefaultState', function() {
    checkAdapterFieldSet(adapterFieldSet.value);
  });

  test('AdapterPage_AdapterChanged', function() {
    const adapterInfo = adapterFieldSet.value;

    adapterInfo.present = !adapterInfo.present;
    adapterBroker.presentChanged(adapterInfo.present);
    checkAdapterFieldSet(adapterInfo);

    adapterInfo.discovering = !adapterInfo.discovering;
    adapterBroker.discoveringChanged(adapterInfo.discovering);
    checkAdapterFieldSet(adapterInfo);
  });

  test('AdapterPage_AdapterChanged_RepeatTwice', function() {
    const adapterInfo = adapterFieldSet.value;

    adapterInfo.present = !adapterInfo.present;
    adapterBroker.presentChanged(adapterInfo.present);
    checkAdapterFieldSet(adapterInfo);
    adapterBroker.presentChanged(adapterInfo.present);
    checkAdapterFieldSet(adapterInfo);

    adapterInfo.discovering = !adapterInfo.discovering;
    adapterBroker.discoveringChanged(adapterInfo.discovering);
    checkAdapterFieldSet(adapterInfo);
    adapterBroker.discoveringChanged(adapterInfo.discovering);
    checkAdapterFieldSet(adapterInfo);
  });

  /** Device Details Page Tests */

  /**
   * Checks DeviceDetailsPage status fieldset.
   * @param {!HTMLElement} detailsPage
   * @param {!Object} deviceInfo
   */
  function checkDeviceDetailsFieldSet(detailsPage, deviceInfo) {
    ['name',
     'address',
     'isGattConnected',
     'rssi.value',
     'serviceUuids',
     'manufacturerDataMap',
    ].forEach(function(propName) {
      const valueCell =
          detailsPage.querySelector('object-field-set')
              .shadowRoot.querySelector(`fieldset [data-field="${propName}"]`);

      const parts = propName.split('.');
      let value = deviceInfo;

      while (value != null && parts.length > 0) {
        const part = parts.shift();
        value = value[part];
      }

      if (propName === 'isGattConnected') {
        value = value ? 'Connected' : 'Not Connected';
      } else if (propName === 'serviceUuids') {
        value = formatServiceUuids(value);
      } else if (propName === 'manufacturerDataMap') {
        value = formatManufacturerDataMap(value);
      }

      if (typeof (value) === 'boolean') {
        assertEquals(value, valueCell.classList.contains('checked'));
      } else if (typeof (value) === 'string') {
        assertEquals(value, valueCell.textContent);
      } else if (typeof (value) === 'number') {
        assertEquals(value.toString(), valueCell.textContent);
      } else {
        assertNotReached(
            'boolean, number or string type expected but got ' +
            typeof (value));
      }
    });
  }

  test('DeviceDetailsPage_NewDelete', function() {
    const device = devices.item(0);

    // Have to search manually since the device row IDs aren't valid selectors.
    const deviceRow = Array.from(deviceTable.querySelectorAll('tr'))
                          .find(row => row.id === device.address);
    const deviceInspectLink = deviceRow.querySelector('[is="action-link"]');

    const deviceDetailsPageId = 'devices/' + device.address.toLowerCase();

    deviceInspectLink.click();
    assertEquals('#' + deviceDetailsPageId, window.location.hash);

    let detailsPage = document.getElementById(deviceDetailsPageId);
    assertTrue(!!detailsPage);

    return internalsHandler.adapter.deviceImplMap.get(device.address)
        .whenCalled('getServices')
        .then(function() {
          // At this point, the device details page should be fully loaded.
          checkDeviceDetailsFieldSet(detailsPage, device);

          detailsPage.querySelector('.forget').click();
          assertEquals('#devices', window.location.hash);
          detailsPage = document.getElementById(deviceDetailsPageId);
          assertFalse(!!detailsPage);
        });
  });

  test('DeviceDetailsPage_NewDelete_FromDevicesPage', function() {
    const device = devices.item(0);
    const deviceDetailsPageId = 'devices/' + device.address.toLowerCase();

    const deviceRow = Array.from(deviceTable.querySelectorAll('tr'))
                          .find(row => row.id === device.address);
    const deviceLinks = deviceRow.querySelectorAll('[is="action-link"]');

    // First link is 'Inspect'.
    deviceLinks[0].click();
    assertEquals('#' + deviceDetailsPageId, window.location.hash);

    let detailsPage = document.getElementById(deviceDetailsPageId);
    assertTrue(!!detailsPage);

    return internalsHandler.adapter.deviceImplMap.get(device.address)
        .whenCalled('getServices')
        .then(function() {
          // At this point, the device details page should be fully loaded.
          checkDeviceDetailsFieldSet(detailsPage, device);

          // Second link is 'Forget'.
          deviceLinks[1].click();
          assertEquals('#devices', window.location.hash);
          detailsPage = document.getElementById(deviceDetailsPageId);
          assertFalse(!!detailsPage);
        });
  });

  test('CheckSystemPermissions_need_location_permission', async function() {
    internalsHandler.setSystemPermission(
        /*needLocationPermission=*/ true,
        /*needNearbyDevicesPermission=*/ false,
        /*needLocationServices=*/ false,
        /*canRequestPermissions=*/ true,
    );
    await checkSystemPermissions(bluetoothInternalsHandlerRemote, () => {
      assert(false);
    });
    await internalsHandler.whenCalled('checkSystemPermissions');
    assertTrue(document.getElementById('need-location-permission').open);
    document.getElementById('need-location-permission-permission-link').click();
    await internalsHandler.whenCalled('requestSystemPermissions');
    assertFalse(document.getElementById('need-location-permission').open);
    assertTrue(document.getElementById('refresh-page').open);
  });

  test(
      'CheckSystemPermissions_need_location_permission_and_services_on_' +
          'click_permission_link',
      async function() {
        internalsHandler.setSystemPermission(
            /*needLocationPermission=*/ true,
            /*needNearbyDevicesPermission=*/ false,
            /*needLocationServices=*/ true,
            /*canRequestPermissions=*/ true,
        );
        await checkSystemPermissions(bluetoothInternalsHandlerRemote, () => {
          assert(false);
        });
        await internalsHandler.whenCalled('checkSystemPermissions');
        assertTrue(
            document.getElementById('need-location-permission-and-services-on')
                .open);
        document
            .getElementById(
                'need-location-permission-and-services-on-permission-link')
            .click();
        await internalsHandler.whenCalled('requestSystemPermissions');
        assertFalse(
            document.getElementById('need-location-permission-and-services-on')
                .open);
        assertTrue(document.getElementById('refresh-page').open);
      });

  test(
      'CheckSystemPermissions_need_location_permission_and_services_on_' +
          'click_services_link',
      async function() {
        internalsHandler.setSystemPermission(
            /*needLocationPermission=*/ true,
            /*needNearbyDevicesPermission=*/ false,
            /*needLocationServices=*/ true,
            /*canRequestPermissions=*/ true,
        );
        await checkSystemPermissions(bluetoothInternalsHandlerRemote, () => {
          assert(false);
        });
        await internalsHandler.whenCalled('checkSystemPermissions');
        assertTrue(
            document.getElementById('need-location-permission-and-services-on')
                .open);
        document
            .getElementById(
                'need-location-permission-and-services-on-services-link')
            .click();
        await internalsHandler.whenCalled('requestLocationServices');
        assertFalse(
            document.getElementById('need-location-permission-and-services-on')
                .open);
        assertTrue(document.getElementById('refresh-page').open);
      });

  test(
      'CheckSystemPermissions_need_nearby_devices_permission',
      async function() {
        internalsHandler.setSystemPermission(
            /*needLocationPermission=*/ false,
            /*needNearbyDevicesPermission=*/ true,
            /*needLocationServices=*/ false,
            /*canRequestPermissions=*/ true,
        );
        await checkSystemPermissions(bluetoothInternalsHandlerRemote, () => {
          assert(false);
        });
        await internalsHandler.whenCalled('checkSystemPermissions');
        assertTrue(
            document.getElementById('need-nearby-devices-permission').open);
        document
            .getElementById('need-nearby-devices-permission-permission-link')
            .click();
        await internalsHandler.whenCalled('requestSystemPermissions');
        assertFalse(
            document.getElementById('need-nearby-devices-permission').open);
        assertTrue(document.getElementById('refresh-page').open);
      });

  test('CheckSystemPermissions_can_not_request_permission', async function() {
    internalsHandler.setSystemPermission(
        /*needLocationPermission=*/ false,
        /*needNearbyDevicesPermission=*/ true,
        /*needLocationServices=*/ false,
        /*canRequestPermissions=*/ false,
    );
    await checkSystemPermissions(bluetoothInternalsHandlerRemote, () => {
      assert(false);
    });
    await internalsHandler.whenCalled('checkSystemPermissions');
    assertTrue(document.getElementById('can-not-request-permissions').open);
  });

  // <if expr="chromeos_ash">
  test('Restart system Bluetooth', async function() {
    const getReStartBluetoothBtn = () => {
      return document.querySelector('#restart-bluetooth-btn');
    };

    assert(getReStartBluetoothBtn());
    assertEquals(
        getReStartBluetoothBtn().textContent, 'Restart system Bluetooth');

    getReStartBluetoothBtn().click();
    await flushTasks();
    await internalsHandler.whenCalled('restartSystemBluetooth');

    assertEquals(
        getReStartBluetoothBtn().textContent, 'Restarting system Bluetooth..');

    internalsHandler.completeRestartSystemBluetooth();
    await flushTasks();
    await internalsHandler.whenCalled('completeRestartSystemBluetooth');
    assertEquals(
        getReStartBluetoothBtn().textContent, 'Restart system Bluetooth');
  });
  // </if>
});

suite('BluetoothInternalsUnitTests', function() {
  /* Value Control Unit Tests */
  const aCode = 'a'.charCodeAt(0);
  const bCode = 'b'.charCodeAt(0);
  const cCode = 'c'.charCodeAt(0);

  const device1 = fakeDeviceInfo1();
  const service1 = fakeServiceInfo1();
  const characteristic1 = fakeCharacteristicInfo1();
  let valueControl = null;

  setup(function() {
    document.body.innerHTML = window.trustedTypes.emptyHTML;
    valueControl = document.createElement('value-control');
    document.body.appendChild(valueControl);
    valueControl.dataset.options = JSON.stringify({
      deviceAddress: device1.address,
      serviceId: service1.id,
      characteristicId: characteristic1,
    });
    valueControl.shadowRoot.querySelector('select').value =
        ValueDataType.HEXADECIMAL;
  });

  test('ValueControl_SetValue_Hexadecimal_EmptyArray', function() {
    valueControl.dataset.value = JSON.stringify([]);
    assertEquals('', valueControl.shadowRoot.querySelector('input').value);
  });

  test('ValueControl_SetValue_Hexadecimal_OneValue', function() {
    valueControl.dataset.value = JSON.stringify([aCode]);
    assertEquals('0x61', valueControl.shadowRoot.querySelector('input').value);
  });

  test('ValueControl_SetValue_Hexadecimal_ThreeValues', function() {
    valueControl.dataset.value = JSON.stringify([aCode, bCode, cCode]);
    assertEquals(
        '0x616263', valueControl.shadowRoot.querySelector('input').value);
  });

  test('ValueControl_SetValue_UTF8_EmptyArray', function() {
    valueControl.shadowRoot.querySelector('select').value = ValueDataType.UTF8;
    valueControl.dataset.value = JSON.stringify([]);
    assertEquals('', valueControl.shadowRoot.querySelector('input').value);
  });

  test('ValueControl_SetValue_UTF8_OneValue', function() {
    valueControl.shadowRoot.querySelector('select').value = ValueDataType.UTF8;
    valueControl.dataset.value = JSON.stringify([aCode]);
    assertEquals('a', valueControl.shadowRoot.querySelector('input').value);
  });

  test('ValueControl_SetValue_UTF8_ThreeValues', function() {
    valueControl.shadowRoot.querySelector('select').value = ValueDataType.UTF8;
    valueControl.dataset.value = JSON.stringify([aCode, bCode, cCode]);
    assertEquals('abc', valueControl.shadowRoot.querySelector('input').value);
  });

  test('ValueControl_SetValue_Decimal_EmptyArray', function() {
    valueControl.shadowRoot.querySelector('select').value =
        ValueDataType.DECIMAL;
    valueControl.dataset.value = JSON.stringify([]);
    assertEquals('', valueControl.shadowRoot.querySelector('input').value);
  });

  test('ValueControl_SetValue_Decimal_OneValue', function() {
    valueControl.shadowRoot.querySelector('select').value =
        ValueDataType.DECIMAL;
    valueControl.dataset.value = JSON.stringify([aCode]);
    assertEquals(
        String(aCode), valueControl.shadowRoot.querySelector('input').value);
  });

  test('ValueControl_SetValue_Decimal_ThreeValues', function() {
    valueControl.shadowRoot.querySelector('select').value =
        ValueDataType.DECIMAL;
    valueControl.dataset.value = JSON.stringify([aCode, bCode, cCode]);
    assertEquals(
        '97-98-99', valueControl.shadowRoot.querySelector('input').value);
  });

  test('ValueControl_ConvertValue_Hexadecimal_EmptyString', function() {
    valueControl.value_.setAs(ValueDataType.HEXADECIMAL, '');
    assertEquals(0, valueControl.value_.getArray().length);
  });

  test('ValueControl_ConvertValue_Hexadecimal_BadHexPrefix', function() {
    assertThrows(function() {
      valueControl.value_.setAs(ValueDataType.HEXADECIMAL, 'd0x');
    }, 'Expected new value to start with "0x"');
  });

  test('ValueControl_ConvertValue_Hexadecimal_ThreeValues', function() {
    valueControl.value_.setAs(ValueDataType.HEXADECIMAL, '0x616263');
    assertDeepEquals([aCode, bCode, cCode], valueControl.value_.getArray());
  });

  test('ValueControl_ConvertValue_UTF8_EmptyString', function() {
    valueControl.shadowRoot.querySelector('select').value = ValueDataType.UTF8;
    valueControl.value_.setAs(ValueDataType.UTF8, '');
    assertEquals(0, valueControl.value_.getArray().length);
  });

  test('ValueControl_ConvertValue_UTF8_ThreeValues', function() {
    valueControl.shadowRoot.querySelector('select').value = ValueDataType.UTF8;
    valueControl.value_.setAs(ValueDataType.UTF8, 'abc');
    assertDeepEquals([aCode, bCode, cCode], valueControl.value_.getArray());
  });

  test('ValueControl_ConvertValue_Decimal_EmptyString', function() {
    valueControl.shadowRoot.querySelector('select').value =
        ValueDataType.DECIMAL;
    valueControl.value_.setAs(ValueDataType.DECIMAL, '');
    assertEquals(0, valueControl.value_.getArray().length);
  });

  test('ValueControl_ConvertValue_Decimal_ThreeValues_Fail', function() {
    valueControl.shadowRoot.querySelector('select').value =
        ValueDataType.DECIMAL;

    assertThrows(function() {
      valueControl.value_.setAs(ValueDataType.DECIMAL, '97-+-99' /* a-+-c */);
    }, 'New value can only contain numbers and hyphens');
  });

  test('ValueControl_ConvertValue_Decimal_ThreeValues', function() {
    valueControl.shadowRoot.querySelector('select').value =
        ValueDataType.DECIMAL;
    valueControl.value_.setAs(ValueDataType.DECIMAL, '97-98-99' /* abc */);
    assertDeepEquals([aCode, bCode, cCode], valueControl.value_.getArray());
  });
});