[USB Internals Test] Add test for rendering device descriptors

Added UI test with same logic of user's journey.
- Added test for rendering a new tab after user inspecting a device.
- Added test for displaying the device descriptors correcty with correct
  response and short response. Make sure two views and error can
  displayed well, and the mapping effect works.
Also added fake UsbDeviceProxy class to implement the functions that
needed for test.

Bug: 928923
Change-Id: Ifb87ef48eb42eb515937607cda0a2d1c433f3941
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1616382
Commit-Queue: Nancy Li <nancyly@google.com>
Reviewed-by: Matt Reynolds <mattreynolds@chromium.org>
Reviewed-by: Reilly Grant <reillyg@chromium.org>
Cr-Commit-Position: refs/heads/master@{#663360}
diff --git a/chrome/browser/resources/usb_internals/descriptor_panel.js b/chrome/browser/resources/usb_internals/descriptor_panel.js
index f0a2cbf..f894ea5 100644
--- a/chrome/browser/resources/usb_internals/descriptor_panel.js
+++ b/chrome/browser/resources/usb_internals/descriptor_panel.js
@@ -190,23 +190,32 @@
 
       for (const field of fields) {
         const className = `field-offset-${offset}`;
+        let item;
+        try {
+          item = customTreeItem(
+              `${field.label}${field.formatter(rawData, offset)}`, className);
 
-        const item = customTreeItem(
-            `${field.label}${field.formatter(rawData, offset)}`, className);
-
-        if (field.extraTreeItemFormatter) {
-          field.extraTreeItemFormatter(rawData, offset, item, field.label);
+          for (let i = 0; i < field.size; i++) {
+            rawDataByteElements[offset + i].classList.add(className);
+            for (const parentClassName of parentClassNames) {
+              rawDataByteElements[offset + i].classList.add(parentClassName);
+            }
+          }
+        } catch (e) {
+          this.showError_(`Field at offset ${offset} is invalid.`);
+          break;
         }
 
-        for (let i = 0; i < field.size; i++) {
-          rawDataByteElements[offset + i].classList.add(className);
-          for (const parentClassName of parentClassNames) {
-            rawDataByteElements[offset + i].classList.add(parentClassName);
+        try {
+          if (field.extraTreeItemFormatter) {
+            field.extraTreeItemFormatter(rawData, offset, item, field.label);
           }
+        } catch (e) {
+          this.showError_(
+              `Error at rendering field at index ${offset}: ${e.message}`);
         }
 
         root.add(item);
-
         offset += field.size;
       }
 
@@ -461,7 +470,7 @@
      * @private
      */
     async getDeviceDescriptor_() {
-      /** @type {device.mojom.UsbControlTransferParams} */
+      /** @type {!device.mojom.UsbControlTransferParams} */
       const usbControlTransferParams = {};
       usbControlTransferParams.type =
           device.mojom.UsbControlTransferType.STANDARD;
@@ -582,14 +591,14 @@
 
       renderRawDataBytes(rawDataByteElement, rawData);
 
-      const offset = this.renderRawDataTree_(
+      this.renderRawDataTree_(
           rawDataTreeRoot, rawDataByteElement, fields, rawData, 0);
 
-      assert(
-          offset === DEVICE_DESCRIPTOR_LENGTH,
-          'Device Descriptor Rendering Error');
-
       addMappingAction(rawDataTreeRoot, rawDataByteElement);
+
+      // window.deviceDescriptorCompleteFn() provides a hook for the test suite
+      // to perform test actions after the device descriptor is rendered.
+      window.deviceDescriptorCompleteFn();
     }
 
     /**
@@ -598,7 +607,7 @@
      * @private
      */
     async getConfigurationDescriptor_() {
-      /** @type {device.mojom.UsbControlTransferParams} */
+      /** @type {!device.mojom.UsbControlTransferParams} */
       const usbControlTransferParams = {};
       usbControlTransferParams.type =
           device.mojom.UsbControlTransferType.STANDARD;
@@ -754,10 +763,6 @@
             indexEndpoint}.`);
       }
 
-      assert(
-          offset === rawData.length,
-          'Complete Configuration Descriptor Rendering Error');
-
       addMappingAction(rawDataTreeRoot, rawDataByteElement);
     }
 
@@ -985,7 +990,7 @@
      * @return {!Array<string>}
      */
     async getAllLanguageCodes() {
-      /** @type {device.mojom.UsbControlTransferParams} */
+      /** @type {!device.mojom.UsbControlTransferParams} */
       const usbControlTransferParams = {};
       usbControlTransferParams.type =
           device.mojom.UsbControlTransferType.STANDARD;
@@ -1049,7 +1054,7 @@
      * @private
      */
     async getStringDescriptorForLanguageCode_(index, languageCode) {
-      /** @type {device.mojom.UsbControlTransferParams} */
+      /** @type {!device.mojom.UsbControlTransferParams} */
       const usbControlTransferParams = {};
       usbControlTransferParams.type =
           device.mojom.UsbControlTransferType.STANDARD;
@@ -1154,7 +1159,7 @@
 
       renderRawDataBytes(rawDataByteElement, rawData);
 
-      const offset = this.renderRawDataTree_(
+      this.renderRawDataTree_(
           stringDescriptorItem, rawDataByteElement, fields, rawData, 0,
           parentClassName);
 
@@ -1263,7 +1268,7 @@
      * @private
      */
     async getBosDescriptor_() {
-      /** @type {device.mojom.UsbControlTransferParams} */
+      /** @type {!device.mojom.UsbControlTransferParams} */
       const usbControlTransferParams = {};
       usbControlTransferParams.type =
           device.mojom.UsbControlTransferType.STANDARD;
@@ -1396,9 +1401,6 @@
             `${encounteredNumBosDescriptors}.`);
       }
 
-      assert(
-          offset === rawData.length, 'Complete BOS Descriptor Rendering Error');
-
       addMappingAction(rawDataTreeRoot, rawDataByteElement);
     }
 
@@ -1711,7 +1713,7 @@
      * @private
      */
     async getUrlDescriptor_(vendorCode, urlIndex) {
-      /** @type {device.mojom.UsbControlTransferParams} */
+      /** @type {!device.mojom.UsbControlTransferParams} */
       const usbControlTransferParams = {};
       usbControlTransferParams.recipient =
           device.mojom.UsbControlTransferRecipient.DEVICE;
@@ -1770,7 +1772,7 @@
      * @private
      */
     async getMsOs20DescriptorSet_(vendorCode, msOs20DescriptorSetLength) {
-      /** @type {device.mojom.UsbControlTransferParams} */
+      /** @type {!device.mojom.UsbControlTransferParams} */
       const usbControlTransferParams = {};
       usbControlTransferParams.recipient =
           device.mojom.UsbControlTransferRecipient.DEVICE;
@@ -1812,7 +1814,7 @@
      * @private
      */
     async sendMsOs20DescriptorSetAltEnumCommand_(vendorCode, altEnumCode) {
-      /** @type {device.mojom.UsbControlTransferParams} */
+      /** @type {!device.mojom.UsbControlTransferParams} */
       const usbControlTransferParams = {};
       usbControlTransferParams.recipient =
           device.mojom.UsbControlTransferRecipient.DEVICE;
@@ -2614,7 +2616,11 @@
     const rawDataByteElements = rawDataByteElement.querySelectorAll('span');
     rawDataByteElements.forEach((el) => {
       const classList = el.classList;
-
+      if (!classList[0]) {
+        // For a field that has failed to render there might be some leftover
+        // bytes. Just skip them.
+        return;
+      }
       const fieldOffsetClass = classList[0];
       assert(fieldOffsetClass.startsWith('field-offset-'));
 
@@ -2646,8 +2652,7 @@
   }
 
   /**
-   * Renders an element to display the raw data in hex, byte by byte, and
-   * keeps every row no more than 16 bytes.
+   * Renders an element to display the raw data in hex, byte by byte.
    * @param {!HTMLElement} rawDataByteElement
    * @param {!Uint8Array} rawData
    */
@@ -2680,10 +2685,7 @@
    * @return {string}
    */
   function toHex(number, numOfDigits) {
-    return number.toString(16)
-        .padStart(numOfDigits, '0')
-        .slice(0 - numOfDigits)
-        .toUpperCase();
+    return number.toString(16).padStart(numOfDigits, '0').toUpperCase();
   }
 
   /**
@@ -2781,7 +2783,7 @@
    * @return {string}
    */
   function formatBitmap(rawData, offset) {
-    return rawData[offset].toString(2).padStart(8, '0').slice(-8);
+    return rawData[offset].toString(2).padStart(8, '0');
   }
 
   /**
@@ -2934,4 +2936,7 @@
   return {
     DescriptorPanel,
   };
-});
\ No newline at end of file
+});
+
+window.deviceDescriptorCompleteFn =
+    window.deviceDescriptorCompleteFn || function() {};
diff --git a/chrome/browser/resources/usb_internals/devices_page.js b/chrome/browser/resources/usb_internals/devices_page.js
index ed63f67c..f6196e0 100644
--- a/chrome/browser/resources/usb_internals/devices_page.js
+++ b/chrome/browser/resources/usb_internals/devices_page.js
@@ -20,7 +20,6 @@
     constructor(usbManager) {
       /** @private {device.mojom.UsbDeviceManagerProxy} */
       this.usbManager_ = usbManager;
-
       this.renderDeviceList_();
     }
 
@@ -67,7 +66,7 @@
       }
       // window.deviceListCompleteFn() provides a hook for the test suite to
       // perform test actions after the devices list is loaded.
-      await window.deviceListCompleteFn();
+      window.deviceListCompleteFn();
     }
 
     /**
@@ -97,8 +96,7 @@
     constructor(usbManager, device) {
       /** @private {device.mojom.UsbDeviceManagerProxy} */
       this.usbManager_ = usbManager;
-
-      this.renderTab(device);
+      this.renderTab_(device);
     }
 
     /**
@@ -106,7 +104,7 @@
      * @param {!device.mojom.UsbDeviceInfo} device
      * @private
      */
-    renderTab(device) {
+    renderTab_(device) {
       const tabs = document.querySelector('tabs');
 
       const tabTemplate = document.querySelector('#tab-template');
@@ -126,31 +124,44 @@
       cr.ui.decorate('tab', cr.ui.Tab);
 
       const tabPanels = document.querySelector('tabpanels');
-
       const tabPanelTemplate = document.querySelector('#tabpanel-template');
       const tabPanelClone = document.importNode(tabPanelTemplate.content, true);
 
       /**
        * Root of the WebContents tree of current device.
-       * @type {cr.ui.Tree|null}
+       * @type {?cr.ui.Tree}
        */
       const treeViewRoot = tabPanelClone.querySelector('#tree-view');
       cr.ui.decorate(treeViewRoot, cr.ui.Tree);
       treeViewRoot.detail = {payload: {}, children: {}};
       // Clear the tree first before populating it with the new content.
       treeViewRoot.innerText = '';
-      this.renderDeviceTree_(device, treeViewRoot);
+      renderDeviceTree(device, treeViewRoot);
 
+      const tabPanel = tabPanelClone.querySelector('tabpanel');
+      this.initializeDescriptorPanels_(tabPanel, device.guid);
+
+      tabPanels.appendChild(tabPanelClone);
+      cr.ui.decorate(tabPanel, cr.ui.TabPanel);
+    }
+
+    /**
+     * Initializes all the descriptor panels.
+     * @param {!HTMLElement} tabPanel
+     * @param {number} guid
+     * @private
+     */
+    async initializeDescriptorPanels_(tabPanel, guid) {
       const usbDeviceProxy = new UsbDeviceProxy;
-      this.usbManager_.getDevice(device.guid, usbDeviceProxy.$.createRequest());
+      await this.usbManager_.getDevice(guid, usbDeviceProxy.$.createRequest());
 
       const getStringDescriptorButton =
-          tabPanelClone.querySelector('#string-descriptor-button');
+          tabPanel.querySelector('#string-descriptor-button');
       const stringDescriptorElement =
-          tabPanelClone.querySelector('.string-descriptor-panel');
+          tabPanel.querySelector('.string-descriptor-panel');
       const stringDescriptorPanel = new descriptor_panel.DescriptorPanel(
           usbDeviceProxy, stringDescriptorElement);
-      stringDescriptorPanel.initialStringDescriptorPanel(tab.id);
+      stringDescriptorPanel.initialStringDescriptorPanel(guid);
       getStringDescriptorButton.addEventListener('click', () => {
         stringDescriptorElement.hidden = !stringDescriptorElement.hidden;
 
@@ -163,9 +174,9 @@
       });
 
       const getDeviceDescriptorButton =
-          tabPanelClone.querySelector('#device-descriptor-button');
+          tabPanel.querySelector('#device-descriptor-button');
       const deviceDescriptorElement =
-          tabPanelClone.querySelector('.device-descriptor-panel');
+          tabPanel.querySelector('.device-descriptor-panel');
       const deviceDescriptorPanel = new descriptor_panel.DescriptorPanel(
           usbDeviceProxy, deviceDescriptorElement, stringDescriptorPanel);
       getDeviceDescriptorButton.addEventListener('click', () => {
@@ -180,9 +191,9 @@
       });
 
       const getConfigurationDescriptorButton =
-          tabPanelClone.querySelector('#configuration-descriptor-button');
+          tabPanel.querySelector('#configuration-descriptor-button');
       const configurationDescriptorElement =
-          tabPanelClone.querySelector('.configuration-descriptor-panel');
+          tabPanel.querySelector('.configuration-descriptor-panel');
       const configurationDescriptorPanel = new descriptor_panel.DescriptorPanel(
           usbDeviceProxy, configurationDescriptorElement,
           stringDescriptorPanel);
@@ -199,9 +210,9 @@
       });
 
       const getBosDescriptorButton =
-          tabPanelClone.querySelector('#bos-descriptor-button');
+          tabPanel.querySelector('#bos-descriptor-button');
       const bosDescriptorElement =
-          tabPanelClone.querySelector('.bos-descriptor-panel');
+          tabPanel.querySelector('.bos-descriptor-panel');
       const bosDescriptorPanel = new descriptor_panel.DescriptorPanel(
           usbDeviceProxy, bosDescriptorElement);
       getBosDescriptorButton.addEventListener('click', () => {
@@ -214,174 +225,183 @@
           bosDescriptorPanel.renderBosDescriptor();
         }
       });
-
-      tabPanels.appendChild(tabPanelClone);
-      cr.ui.decorate('tabpanel', cr.ui.TabPanel);
+      // window.deviceTabInitializedFn() provides a hook for the test suite to
+      // perform test actions after the device tab query descriptors actions are
+      // initialized.
+      window.deviceTabInitializedFn();
     }
+  }
 
-    /**
-     * Renders a tree to display the device's detail information.
-     * @param {!device.mojom.UsbDeviceInfo} device
-     * @param {!cr.ui.Tree} root
-     * @private
-     */
-    renderDeviceTree_(device, root) {
-      root.add(customTreeItem(`USB Version: ${device.usbVersionMajor}.${
-          device.usbVersionMinor}.${device.usbVersionSubminor}`));
+  /**
+   * Renders a tree to display the device's detail information.
+   * @param {!device.mojom.UsbDeviceInfo} device
+   * @param {!cr.ui.Tree} root
+   */
+  function renderDeviceTree(device, root) {
+    root.add(customTreeItem(`USB Version: ${device.usbVersionMajor}.${
+        device.usbVersionMinor}.${device.usbVersionSubminor}`));
 
-      root.add(customTreeItem(`Class Code: ${device.classCode}`));
+    root.add(customTreeItem(`Class Code: ${device.classCode}`));
 
-      root.add(customTreeItem(`Subclass Code: ${device.subclassCode}`));
+    root.add(customTreeItem(`Subclass Code: ${device.subclassCode}`));
 
-      root.add(customTreeItem(`Protocol Code: ${device.protocolCode}`));
+    root.add(customTreeItem(`Protocol Code: ${device.protocolCode}`));
 
-      root.add(customTreeItem(`Port Number: ${device.portNumber}`));
+    root.add(customTreeItem(`Port Number: ${device.portNumber}`));
 
-      root.add(customTreeItem(`Vendor Id: ${toHex(device.vendorId)}`));
+    root.add(customTreeItem(`Vendor Id: ${toHex(device.vendorId)}`));
 
-      root.add(customTreeItem(`Product Id: ${toHex(device.productId)}`));
+    root.add(customTreeItem(`Product Id: ${toHex(device.productId)}`));
 
-      root.add(customTreeItem(`Device Version: ${device.deviceVersionMajor}.${
-          device.deviceVersionMinor}.${device.deviceVersionSubminor}`));
+    root.add(customTreeItem(`Device Version: ${device.deviceVersionMajor}.${
+        device.deviceVersionMinor}.${device.deviceVersionSubminor}`));
 
+    if (device.manufacturerName) {
       root.add(customTreeItem(`Manufacturer Name: ${
           decodeString16(device.manufacturerName.data)}`));
+    }
 
+    if (device.productName) {
       root.add(customTreeItem(
           `Product Name: ${decodeString16(device.productName.data)}`));
+    }
 
+    if (device.serialNumber) {
       root.add(customTreeItem(
           `Serial Number: ${decodeString16(device.serialNumber.data)}`));
-
-      root.add(customTreeItem(
-          `WebUSB Landing Page: ${device.webusbLandingPage.url}`));
-
-      root.add(customTreeItem(
-          `Active Configuration: ${device.activeConfiguration}`));
-
-      const configurationsArray = device.configurations;
-      this.renderConfigurationTreeItem_(configurationsArray, root);
     }
 
-    /**
-     * Renders a tree item to display the device's configurations information.
-     * @param {!Array<!device.mojom.UsbConfigurationInfo>} configurationsArray
-     * @param {!cr.ui.TreeItem} root
-     * @private
-     */
-    renderConfigurationTreeItem_(configurationsArray, root) {
-      for (const configuration of configurationsArray) {
-        const configurationItem =
-            customTreeItem(`Configuration ${configuration.configurationValue}`);
+    if (device.webusbLandingPage) {
+      const urlItem = customTreeItem(
+          `WebUSB Landing Page: ${device.webusbLandingPage.url}`);
+      root.add(urlItem);
 
-        if (configuration.configurationName) {
-          configurationItem.add(customTreeItem(`Configuration Name: ${
-              decodeString16(configuration.configurationName.data)}`));
-        }
-
-        const interfacesArray = configuration.interfaces;
-        this.renderInterfacesTreeItem_(interfacesArray, configurationItem);
-
-        root.add(configurationItem);
-      }
+      urlItem.querySelector('.tree-label')
+          .addEventListener(
+              'click',
+              () => window.open(device.webusbLandingPage.url, '_blank'));
     }
 
-    /**
-     * Renders a tree item to display the device's interfaces information.
-     * @param {!Array<!device.mojom.UsbInterfaceInfo>} interfacesArray
-     * @param {!cr.ui.TreeItem} root
-     * @private
-     */
-    renderInterfacesTreeItem_(interfacesArray, root) {
-      for (const currentInterface of interfacesArray) {
-        const interfaceItem =
-            customTreeItem(`Interface ${currentInterface.interfaceNumber}`);
+    root.add(
+        customTreeItem(`Active Configuration: ${device.activeConfiguration}`));
 
-        const alternatesArray = currentInterface.alternates;
-        this.renderAlternatesTreeItem_(alternatesArray, interfaceItem);
+    const configurationsArray = device.configurations;
+    renderConfigurationTreeItem(configurationsArray, root);
+  }
 
-        root.add(interfaceItem);
+  /**
+   * Renders a tree item to display the device's configuration information.
+   * @param {!Array<!device.mojom.UsbConfigurationInfo>} configurationsArray
+   * @param {!cr.ui.TreeItem} root
+   */
+  function renderConfigurationTreeItem(configurationsArray, root) {
+    for (const configuration of configurationsArray) {
+      const configurationItem =
+          customTreeItem(`Configuration ${configuration.configurationValue}`);
+
+      if (configuration.configurationName) {
+        configurationItem.add(customTreeItem(`Configuration Name: ${
+            decodeString16(configuration.configurationName.data)}`));
       }
+
+      const interfacesArray = configuration.interfaces;
+      renderInterfacesTreeItem(interfacesArray, configurationItem);
+
+      root.add(configurationItem);
     }
+  }
 
-    /**
-     * Renders a tree item to display the device's alternate interfaces
-     * information.
-     * @param {!Array<!device.mojom.UsbAlternateInterfaceInfo>} alternatesArray
-     * @param {!cr.ui.TreeItem} root
-     * @private
-     */
-    renderAlternatesTreeItem_(alternatesArray, root) {
-      for (const alternate of alternatesArray) {
-        const alternateItem =
-            customTreeItem(`Alternate ${alternate.alternateSetting}`);
+  /**
+   * Renders a tree item to display the device's interface information.
+   * @param {!Array<!device.mojom.UsbInterfaceInfo>} interfacesArray
+   * @param {!cr.ui.TreeItem} root
+   */
+  function renderInterfacesTreeItem(interfacesArray, root) {
+    for (const currentInterface of interfacesArray) {
+      const interfaceItem =
+          customTreeItem(`Interface ${currentInterface.interfaceNumber}`);
 
-        alternateItem.add(customTreeItem(`Class Code: ${alternate.classCode}`));
+      const alternatesArray = currentInterface.alternates;
+      renderAlternatesTreeItem(alternatesArray, interfaceItem);
 
-        alternateItem.add(
-            customTreeItem(`Subclass Code: ${alternate.subclassCode}`));
-
-        alternateItem.add(
-            customTreeItem(`Protocol Code: ${alternate.protocolCode}`));
-
-        if (alternate.interfaceName) {
-          alternateItem.add(customTreeItem(`Interface Name: ${
-              decodeString16(alternate.interfaceName.data)}`));
-        }
-
-        const endpointsArray = alternate.endpoints;
-        this.renderEndpointsTreeItem_(endpointsArray, alternateItem);
-
-        root.add(alternateItem);
-      }
+      root.add(interfaceItem);
     }
+  }
 
-    /**
-     * Renders a tree item to display the device's endpoints information.
-     * @param {!Array<!device.mojom.UsbEndpointInfo>} endpointsArray
-     * @param {!cr.ui.TreeItem} root
-     * @private
-     */
-    renderEndpointsTreeItem_(endpointsArray, root) {
-      for (const endpoint of endpointsArray) {
-        let itemLabel = 'Endpoint ';
+  /**
+   * Renders a tree item to display the device's alternate interfaces
+   * information.
+   * @param {!Array<!device.mojom.UsbAlternateInterfaceInfo>} alternatesArray
+   * @param {!cr.ui.TreeItem} root
+   */
+  function renderAlternatesTreeItem(alternatesArray, root) {
+    for (const alternate of alternatesArray) {
+      const alternateItem =
+          customTreeItem(`Alternate ${alternate.alternateSetting}`);
 
-        itemLabel += endpoint.endpointNumber;
+      alternateItem.add(customTreeItem(`Class Code: ${alternate.classCode}`));
 
-        switch (endpoint.direction) {
-          case device.mojom.UsbTransferDirection.INBOUND:
-            itemLabel += ' (INBOUND)';
-            break;
-          case device.mojom.UsbTransferDirection.OUTBOUND:
-            itemLabel += ' (OUTBOUND)';
-            break;
-        }
+      alternateItem.add(
+          customTreeItem(`Subclass Code: ${alternate.subclassCode}`));
 
-        const endpointItem = customTreeItem(itemLabel);
+      alternateItem.add(
+          customTreeItem(`Protocol Code: ${alternate.protocolCode}`));
 
-        let usbTransferType = '';
-        switch (endpoint.type) {
-          case device.mojom.UsbTransferType.CONTROL:
-            usbTransferType = 'CONTROL';
-            break;
-          case device.mojom.UsbTransferType.ISOCHRONOUS:
-            usbTransferType = 'ISOCHRONOUS';
-            break;
-          case device.mojom.UsbTransferType.BULK:
-            usbTransferType = 'BULK';
-            break;
-          case device.mojom.UsbTransferType.INTERRUPT:
-            usbTransferType = 'INTERRUPT';
-            break;
-        }
-
-        endpointItem.add(
-            customTreeItem(`USB Transfer Type: ${usbTransferType}`));
-
-        endpointItem.add(customTreeItem(`Packet Size: ${endpoint.packetSize}`));
-
-        root.add(endpointItem);
+      if (alternate.interfaceName) {
+        alternateItem.add(customTreeItem(
+            `Interface Name: ${decodeString16(alternate.interfaceName.data)}`));
       }
+
+      const endpointsArray = alternate.endpoints;
+      renderEndpointsTreeItem(endpointsArray, alternateItem);
+
+      root.add(alternateItem);
+    }
+  }
+
+  /**
+   * Renders a tree item to display the device's endpoints information.
+   * @param {!Array<!device.mojom.UsbEndpointInfo>} endpointsArray
+   * @param {!cr.ui.TreeItem} root
+   */
+  function renderEndpointsTreeItem(endpointsArray, root) {
+    for (const endpoint of endpointsArray) {
+      let itemLabel = 'Endpoint ';
+
+      itemLabel += endpoint.endpointNumber;
+
+      switch (endpoint.direction) {
+        case device.mojom.UsbTransferDirection.INBOUND:
+          itemLabel += ' (INBOUND)';
+          break;
+        case device.mojom.UsbTransferDirection.OUTBOUND:
+          itemLabel += ' (OUTBOUND)';
+          break;
+      }
+
+      const endpointItem = customTreeItem(itemLabel);
+
+      let usbTransferType = '';
+      switch (endpoint.type) {
+        case device.mojom.UsbTransferType.CONTROL:
+          usbTransferType = 'CONTROL';
+          break;
+        case device.mojom.UsbTransferType.ISOCHRONOUS:
+          usbTransferType = 'ISOCHRONOUS';
+          break;
+        case device.mojom.UsbTransferType.BULK:
+          usbTransferType = 'BULK';
+          break;
+        case device.mojom.UsbTransferType.INTERRUPT:
+          usbTransferType = 'INTERRUPT';
+          break;
+      }
+
+      endpointItem.add(customTreeItem(`USB Transfer Type: ${usbTransferType}`));
+
+      endpointItem.add(customTreeItem(`Packet Size: ${endpoint.packetSize}`));
+
+      root.add(endpointItem);
     }
   }
 
@@ -400,7 +420,7 @@
    * @return {string}
    */
   function toHex(num) {
-    return `0x${num.toString(16).padStart(4, '0').slice(-4).toUpperCase()}`;
+    return `0x${num.toString(16).padStart(4, '0').toUpperCase()}`;
   }
 
   /**
@@ -422,6 +442,6 @@
   };
 });
 
-window.deviceListCompleteFn = window.deviceListCompleteFn || function() {
-  return Promise.resolve();
-};
\ No newline at end of file
+window.deviceListCompleteFn = window.deviceListCompleteFn || function() {};
+
+window.deviceTabInitializedFn = window.deviceTabInitializedFn || function() {};
\ No newline at end of file
diff --git a/chrome/test/data/webui/usb_internals_browsertest.js b/chrome/test/data/webui/usb_internals_browsertest.js
index 72d1e83..c993737 100644
--- a/chrome/test/data/webui/usb_internals_browsertest.js
+++ b/chrome/test/data/webui/usb_internals_browsertest.js
@@ -14,6 +14,8 @@
 function UsbInternalsTest() {
   this.setupResolver = new PromiseResolver();
   this.deviceManagerGetDevicesResolver = new PromiseResolver();
+  this.deviceTabInitializedResolver = new PromiseResolver();
+  this.deviceDescriptorRenderResolver = new PromiseResolver();
 }
 
 UsbInternalsTest.prototype = {
@@ -52,10 +54,6 @@
         this.methodCalled(
             'bindUsbDeviceManagerInterface', deviceManagerRequest);
         this.deviceManager = new FakeDeviceManagerProxy();
-        this.deviceManager.addTestDevice(
-            FakeDeviceManagerProxy.fakeDeviceInfo(0));
-        this.deviceManager.addTestDevice(
-            FakeDeviceManagerProxy.fakeDeviceInfo(1));
         this.deviceManagerBinding_ =
             new device.mojom.UsbDeviceManager(this.deviceManager);
         this.deviceManagerBinding_.bindHandle(deviceManagerRequest.handle);
@@ -79,49 +77,34 @@
         ]);
 
         this.devices = [];
-      }
-
-      addTestDevice(device) {
-        this.devices.push(device);
+        this.deviceProxyMap = new Map();
+        this.addFakeDevice(
+            fakeDeviceInfo(0), createDeviceWithValidDeviceDescriptor());
+        this.addFakeDevice(
+            fakeDeviceInfo(1), createDeviceWithShortDeviceDescriptor());
       }
 
       /**
-       * Creates a fake device use given number.
-       * @param {number} num
-       * @return {!Object}
+       * Adds a fake device to this device manager.
+       * @param {!Object} device
+       * @param {!FakeUsbDeviceProxy} deviceProxy
        */
-      static fakeDeviceInfo(num) {
-        return {
-          guid: `abcdefgh-ijkl-mnop-qrst-uvwxyz12345${num}`,
-          usbVersionMajor: 2,
-          usbVersionMinor: 0,
-          usbVersionSubminor: 2,
-          classCode: 0,
-          subclassCode: 0,
-          protocolCode: 0,
-          busNumber: num,
-          portNumber: num,
-          vendorId: 0x1050 + num,
-          productId: 0x17EF + num,
-          deviceVersionMajor: 3,
-          deviceVersionMinor: 2,
-          deviceVersionSubminor: 1,
-          manufacturerName: undefined,
-          productName: undefined,
-          serialNumber: undefined,
-          webusbLandingPage: undefined,
-          activeConfiguration: 1,
-          configurations: [],
-        };
+      addFakeDevice(device, deviceProxy) {
+        this.devices.push(device);
+        this.deviceProxyMap.set(device.guid, deviceProxy);
       }
 
       async enumerateDevicesAndSetClient() {}
 
-      async getDevice() {}
+      async getDevice(guid, deviceRequest, deviceClient) {
+        this.methodCalled('getDevice');
+        const deviceProxy = this.deviceProxyMap.get(guid);
+        deviceProxy.router.bindHandle(deviceRequest.handle);
+      }
 
       async getDevices() {
         this.methodCalled('getDevices');
-        return Promise.resolve({results: this.devices});
+        return {results: this.devices};
       }
 
       async checkAccess() {}
@@ -131,9 +114,154 @@
       async setClient() {}
     }
 
+    /** @implements {device.mojom.UsbDeviceProxy} */
+    class FakeUsbDeviceProxy extends TestBrowserProxy {
+      constructor() {
+        super([
+          'open',
+          'close',
+          'controlTransferIn',
+        ]);
+        this.responses = new Map();
+
+        // NOTE: We use the generated CallbackRouter here because
+        // device.mojom.UsbDevice defines lots of methods we don't care to mock
+        // here. UsbDeviceCallbackRouter callback silently discards messages
+        // that have no listeners.
+        this.router = new device.mojom.UsbDeviceCallbackRouter;
+        this.router.open.addListener(async () => {
+          return {error: device.mojom.UsbOpenDeviceError.OK};
+        });
+        this.router.controlTransferIn.addListener(
+            (params, length, timeout) =>
+                this.controlTransferIn(params, length, timeout));
+        this.router.close.addListener(async () => {});
+      }
+
+      async controlTransferIn(params, length, timeout) {
+        const response =
+            this.responses.get(usbControlTransferParamsToString(params));
+        if (!response) {
+          return {
+            status: device.mojom.UsbTransferStatus.TRANSFER_ERROR,
+            data: [],
+          };
+        }
+        response.data = response.data.slice(0, length);
+        return response;
+      }
+
+      /**
+       * Set a response for a given request.
+       * @param {!device.mojom.UsbControlTransferParams} params
+       * @param {!Object} response
+       */
+      setResponse(params, response) {
+        this.responses.set(usbControlTransferParamsToString(params), response);
+      }
+
+      /**
+       * Set the device descriptor the device will respond to queries with.
+       * @param {!Object} response
+       */
+      setDeviceDescriptor(response) {
+        const params = {};
+        params.type = device.mojom.UsbControlTransferType.STANDARD;
+        params.recipient = device.mojom.UsbControlTransferRecipient.DEVICE;
+        params.request = 6;
+        params.index = 0;
+        params.value = (1 << 8);
+        this.setResponse(params, response);
+      }
+    }
+
+    /**
+     * Creates a fake device using the given number.
+     * @param {number} num
+     * @return {!Object}
+     */
+    function fakeDeviceInfo(num) {
+      return {
+        guid: `abcdefgh-ijkl-mnop-qrst-uvwxyz12345${num}`,
+        usbVersionMajor: 2,
+        usbVersionMinor: 0,
+        usbVersionSubminor: num,
+        classCode: 0,
+        subclassCode: 0,
+        protocolCode: 0,
+        busNumber: num,
+        portNumber: num,
+        vendorId: 0x1050 + num,
+        productId: 0x17EF + num,
+        deviceVersionMajor: 3,
+        deviceVersionMinor: 2,
+        deviceVersionSubminor: 1,
+        manufacturerName: stringToMojoString16('test'),
+        productName: undefined,
+        serialNumber: undefined,
+        webusbLandingPage: {url: 'http://google.com'},
+        activeConfiguration: 1,
+        configurations: [],
+      };
+    }
+
+    /**
+     * Creates a device with correct descriptors.
+     */
+    function createDeviceWithValidDeviceDescriptor() {
+      const deviceProxy = new FakeUsbDeviceProxy();
+      deviceProxy.setDeviceDescriptor({
+        status: device.mojom.UsbTransferStatus.COMPLETED,
+        data: [
+          0x12, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x40, 0x50, 0x10, 0xEF,
+          0x17, 0x21, 0x03, 0x01, 0x02, 0x00, 0x01
+        ],
+      });
+      return deviceProxy;
+    }
+
+    /**
+     * Creates a device with too short descriptors.
+     */
+    function createDeviceWithShortDeviceDescriptor() {
+      const deviceProxy = new FakeUsbDeviceProxy();
+      deviceProxy.setDeviceDescriptor({
+        status: device.mojom.UsbTransferStatus.SHORT_PACKET,
+        data: [0x12, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x40, 0x50],
+      });
+      return deviceProxy;
+    }
+
+    /**
+     * Converts an ECMAScript string to an instance of mojo_base.mojom.String16.
+     * @param {string} string
+     * @return {!object}
+     */
+    function stringToMojoString16(string) {
+      return {data: Array.from(string, c => c.charCodeAt(0))};
+    }
+
+    /**
+     * Stringify a UsbControlTransferParams type object to be the key of
+     * response map.
+     * @param {!device.mojom.UsbControlTransferParams} params
+     * @return {string}
+     */
+    function usbControlTransferParamsToString(params) {
+      return `${params.type}-${params.recipient}-${params.request}-${
+          params.value}-${params.index}`;
+    }
+
     window.deviceListCompleteFn = () => {
       this.deviceManagerGetDevicesResolver.resolve();
-      return Promise.resolve();
+    };
+
+    window.deviceTabInitializedFn = () => {
+      this.deviceTabInitializedResolver.resolve();
+    };
+
+    window.deviceDescriptorCompleteFn = () => {
+      this.deviceDescriptorRenderResolver.resolve();
     };
 
     window.setupFn = () => {
@@ -153,7 +281,7 @@
   },
 };
 
-TEST_F('UsbInternalsTest', 'WebUITest', function() {
+TEST_F('UsbInternalsTest', 'WebUICorrectValueRenderTest', function() {
   let pageHandler;
 
   // Before tests are run, make sure setup completes.
@@ -163,8 +291,11 @@
 
   let deviceManagerGetDevicesPromise =
       this.deviceManagerGetDevicesResolver.promise;
+  let deviceTabInitializedPromise = this.deviceTabInitializedResolver.promise;
+  let deviceDescriptorRenderPromise =
+      this.deviceDescriptorRenderResolver.promise;
 
-  suite('UsbtoothInternalsUITest', function() {
+  suite('UsbInternalsUITest', function() {
     const EXPECT_DEVICES_NUM = 2;
 
     suiteSetup(function() {
@@ -188,8 +319,8 @@
       // Only 2 tabs after loading page.
       const tabs = document.querySelectorAll('tab');
       expectEquals(2, tabs.length);
-      const tabpanels = document.querySelectorAll('tabpanel');
-      expectEquals(2, tabpanels.length);
+      const tabPanels = document.querySelectorAll('tabpanel');
+      expectEquals(2, tabPanels.length);
 
       // The second is the devices table, which has 8 columns.
       const devicesTable = document.querySelectorAll('table')[1];
@@ -202,6 +333,233 @@
       const devices = devicesTable.querySelectorAll('tbody tr');
       expectEquals(EXPECT_DEVICES_NUM, devices.length);
     });
+
+    test('DeviceTabAdded', function() {
+      const devicesTable = document.querySelector('#device-list');
+      // Click the inspect button to open information about the first device.
+      // The device info is opened as a third tab panel.
+      devicesTable.querySelectorAll('button')[0].click();
+      assertEquals(3, document.querySelectorAll('tab').length);
+      assertEquals(3, document.querySelectorAll('tabpanel').length);
+      expectTrue(document.querySelectorAll('tabpanel')[2].selected);
+
+      // Check that clicking the inspect button for another device will open a
+      // new tabpanel.
+      devicesTable.querySelectorAll('button')[1].click();
+      assertEquals(4, document.querySelectorAll('tab').length);
+      assertEquals(4, document.querySelectorAll('tabpanel').length);
+      expectTrue(document.querySelectorAll('tabpanel')[3].selected);
+      expectFalse(document.querySelectorAll('tabpanel')[2].selected);
+
+      // Check that clicking the inspect button for the same device a second
+      // time will open the same tabpanel.
+      devicesTable.querySelectorAll('button')[0].click();
+      assertEquals(4, document.querySelectorAll('tab').length);
+      assertEquals(4, document.querySelectorAll('tabpanel').length);
+      expectTrue(document.querySelectorAll('tabpanel')[2].selected);
+      expectFalse(document.querySelectorAll('tabpanel')[3].selected);
+    });
+
+    test('RenderDeviceInfoTree', function() {
+      // Test the tab opened by clicking inspect button contains a tree view
+      // showing WebUSB information. Check the tree displays correct data.
+      // The tab panel of the first device is opened in previous test as the
+      // third tab panel.
+      const deviceTab = document.querySelectorAll('tabpanel')[2];
+      const tree = deviceTab.querySelector('tree');
+      const treeItems = tree.querySelectorAll('.tree-item');
+      assertEquals(11, treeItems.length);
+      expectEquals('USB Version: 2.0.0', treeItems[0].textContent);
+      expectEquals('Class Code: 0', treeItems[1].textContent);
+      expectEquals('Subclass Code: 0', treeItems[2].textContent);
+      expectEquals('Protocol Code: 0', treeItems[3].textContent);
+      expectEquals('Port Number: 0', treeItems[4].textContent);
+      expectEquals('Vendor Id: 0x1050', treeItems[5].textContent);
+      expectEquals('Product Id: 0x17EF', treeItems[6].textContent);
+      expectEquals('Device Version: 3.2.1', treeItems[7].textContent);
+      expectEquals('Manufacturer Name: test', treeItems[8].textContent);
+      expectEquals(
+          'WebUSB Landing Page: http://google.com', treeItems[9].textContent);
+      expectEquals('Active Configuration: 1', treeItems[10].textContent);
+    });
+
+    test('RenderDeviceDescriptor', async function() {
+      // Test the tab opened by clicking inspect button contains a panel that
+      // can manually retrieve device descriptor from device. Check the response
+      // can be rendered correctly.
+      await deviceTabInitializedPromise;
+      // The tab panel of the first device is opened in previous test as the
+      // third tab panel. This device has correct device descriptor.
+      const deviceTab = document.querySelectorAll('tabpanel')[2];
+      deviceTab.querySelector('#device-descriptor-button').click();
+
+      await deviceDescriptorRenderPromise;
+      const panel = deviceTab.querySelector('.device-descriptor-panel');
+      expectEquals(1, panel.querySelectorAll('descriptorpanel').length);
+      expectEquals(0, panel.querySelectorAll('error').length);
+      const treeItems = panel.querySelectorAll('.tree-item');
+      assertEquals(14, treeItems.length);
+      expectEquals('Length (should be 18): 18', treeItems[0].textContent);
+      expectEquals(
+          'Descriptor Type (should be 0x01): 0x01', treeItems[1].textContent);
+      expectEquals('USB Version: 2.0.0', treeItems[2].textContent);
+      expectEquals('Class Code: 0', treeItems[3].textContent);
+      expectEquals('Subclass Code: 0', treeItems[4].textContent);
+      expectEquals('Protocol Code: 0', treeItems[5].textContent);
+      expectEquals(
+          'Control Pipe Maximum Packet Size: 64', treeItems[6].textContent);
+      expectEquals('Vendor ID: 0x1050', treeItems[7].textContent);
+      expectEquals('Product ID: 0x17EF', treeItems[8].textContent);
+      expectEquals('Device Version: 3.2.1', treeItems[9].textContent);
+      // The string descriptor index fields with non-zero number should have a
+      // "GET" button.
+      expectEquals(
+          'Manufacturer String Index: 1GET', treeItems[10].textContent);
+      expectEquals('Product String Index: 2GET', treeItems[11].textContent);
+      expectEquals('Serial Number Index: 0', treeItems[12].textContent);
+      expectEquals('Number of Configurations: 1', treeItems[13].textContent);
+      const byteElements = panel.querySelectorAll('#raw-data-byte-view span');
+      expectEquals(18, byteElements.length);
+      expectEquals(
+          '12010002000000405010EF17210301020001',
+          panel.querySelector('#raw-data-byte-view').textContent);
+
+      // Click a single byte tree item (Length) and check that both the item
+      // and the related byte are highlighted.
+      treeItems[0].querySelector('.tree-row').click();
+      expectTrue(treeItems[0].selected);
+      expectTrue(byteElements[0].classList.contains('selected-field'));
+      // Click a multi-byte tree item (Vendor ID) and check that both the
+      // item and the related bytes are highlighted, and other items and bytes
+      // are not highlighted.
+      treeItems[7].querySelector('.tree-row').click();
+      expectFalse(treeItems[0].selected);
+      expectTrue(treeItems[7].selected);
+      expectFalse(byteElements[0].classList.contains('selected-field'));
+      expectTrue(byteElements[8].classList.contains('selected-field'));
+      expectTrue(byteElements[9].classList.contains('selected-field'));
+      // Click a single byte element (Descriptor Type) and check that both the
+      // byte and the related item are highlighted, and other items and bytes
+      // are not highlighted.
+      byteElements[1].click();
+      expectFalse(treeItems[7].selected);
+      expectTrue(treeItems[1].selected);
+      expectTrue(byteElements[1].classList.contains('selected-field'));
+      // Click any byte element of a multi-byte element (Product ID) and check
+      // that both the bytes and the related item are highlighted, and other
+      // items and bytes are not highlighted.
+      byteElements[11].click();
+      expectFalse(treeItems[1].selected);
+      expectTrue(treeItems[8].selected);
+      expectTrue(byteElements[10].classList.contains('selected-field'));
+      expectTrue(byteElements[11].classList.contains('selected-field'));
+    });
+  });
+
+  // Run all registered tests.
+  mocha.run();
+});
+
+TEST_F('UsbInternalsTest', 'WebUIIncorrectValueRenderTest', function() {
+  let pageHandler;
+
+  // Before tests are run, make sure setup completes.
+  let setupPromise = this.setupResolver.promise.then(() => {
+    pageHandler = this.pageHandler;
+  });
+
+  let deviceManagerGetDevicesPromise =
+      this.deviceManagerGetDevicesResolver.promise;
+  let deviceTabInitializedPromise = this.deviceTabInitializedResolver.promise;
+  let deviceDescriptorRenderPromise =
+      this.deviceDescriptorRenderResolver.promise;
+
+  suite('UsbInternalsUITest', function() {
+    suiteSetup(function() {
+      return setupPromise.then(function() {
+        return Promise.all([
+          pageHandler.whenCalled('bindUsbDeviceManagerInterface'),
+          pageHandler.deviceManager.whenCalled('getDevices'),
+        ]);
+      });
+    });
+
+    test('RenderShortDeviceDescriptor', async function() {
+      await deviceManagerGetDevicesPromise;
+      const devicesTable = document.querySelector('#device-list');
+      // Inspect the second device, which has short device descriptor.
+      devicesTable.querySelectorAll('button')[1].click();
+      // The third is the device tab.
+      const deviceTab = document.querySelectorAll('tabpanel')[2];
+
+      await deviceTabInitializedPromise;
+      deviceTab.querySelector('#device-descriptor-button').click();
+
+      await deviceDescriptorRenderPromise;
+      const panel = deviceTab.querySelector('.device-descriptor-panel');
+
+      expectEquals(1, panel.querySelectorAll('descriptorpanel').length);
+      const errors = panel.querySelectorAll('error');
+      assertEquals(2, errors.length);
+      expectEquals('Field at offset 8 is invalid.', errors[0].textContent);
+      expectEquals('Descriptor is too short.', errors[1].textContent);
+      // For the short response, the returned data should still be rendered.
+      const treeItems = panel.querySelectorAll('.tree-item');
+      assertEquals(7, treeItems.length);
+      expectEquals('Length (should be 18): 18', treeItems[0].textContent);
+      expectEquals(
+          'Descriptor Type (should be 0x01): 0x01', treeItems[1].textContent);
+      expectEquals('USB Version: 2.0.0', treeItems[2].textContent);
+      expectEquals('Class Code: 0', treeItems[3].textContent);
+      expectEquals('Subclass Code: 0', treeItems[4].textContent);
+      expectEquals('Protocol Code: 0', treeItems[5].textContent);
+      expectEquals(
+          'Control Pipe Maximum Packet Size: 64', treeItems[6].textContent);
+
+      const byteElements = panel.querySelectorAll('#raw-data-byte-view span');
+      expectEquals(9, byteElements.length);
+      expectEquals(
+          '120100020000004050',
+          panel.querySelector('#raw-data-byte-view').textContent);
+
+
+      // Click a single byte tree item (Length) and check that both the item
+      // and the related byte are highlighted.
+      treeItems[0].querySelector('.tree-row').click();
+      expectTrue(treeItems[0].selected);
+      expectTrue(byteElements[0].classList.contains('selected-field'));
+      // Click a multi-byte tree item (USB Version) and check that both the
+      // item and the related bytes are highlighted, and other items and bytes
+      // are not highlighted.
+      treeItems[2].querySelector('.tree-row').click();
+      expectFalse(treeItems[0].selected);
+      expectTrue(treeItems[2].selected);
+      expectFalse(byteElements[0].classList.contains('selected-field'));
+      expectTrue(byteElements[2].classList.contains('selected-field'));
+      expectTrue(byteElements[3].classList.contains('selected-field'));
+      // Click a single byte element (Descriptor Type) and check that both the
+      // byte and the related item are highlighted, and other items and bytes
+      // are not highlighted.
+      byteElements[1].click();
+      expectFalse(treeItems[2].selected);
+      expectTrue(treeItems[1].selected);
+      expectTrue(byteElements[1].classList.contains('selected-field'));
+      // Click any byte element of a multi-byte element (USB Version) and
+      // check that both the bytes and the related item are highlighted, and
+      // other items and bytes are not highlighted.
+      byteElements[3].click();
+      expectFalse(treeItems[1].selected);
+      expectTrue(treeItems[2].selected);
+      expectTrue(byteElements[2].classList.contains('selected-field'));
+      expectTrue(byteElements[3].classList.contains('selected-field'));
+      // Click the invalid field's byte (Vendor ID) will do nothing, check the
+      // highlighted item and bytes are not changed.
+      byteElements[8].click();
+      expectTrue(treeItems[2].selected);
+      expectTrue(byteElements[2].classList.contains('selected-field'));
+      expectTrue(byteElements[3].classList.contains('selected-field'));
+      expectFalse(byteElements[8].classList.contains('selected-field'));
+    });
   });
 
   // Run all registered tests.