summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webusb
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/webusb')
-rw-r--r--testing/web-platform/tests/webusb/META.yml3
-rw-r--r--testing/web-platform/tests/webusb/README.md17
-rw-r--r--testing/web-platform/tests/webusb/getDevices/reject_opaque_origin.https.html14
-rw-r--r--testing/web-platform/tests/webusb/getDevices/reject_opaque_origin.https.html.headers1
-rw-r--r--testing/web-platform/tests/webusb/getDevices/sandboxed_iframe.https.window.js19
-rw-r--r--testing/web-platform/tests/webusb/idlharness.https.any.js47
-rw-r--r--testing/web-platform/tests/webusb/insecure-context.any.js21
-rw-r--r--testing/web-platform/tests/webusb/protected-interface-classes.https.any.js89
-rw-r--r--testing/web-platform/tests/webusb/requestDevice/reject_opaque_origin.https.html15
-rw-r--r--testing/web-platform/tests/webusb/requestDevice/reject_opaque_origin.https.html.headers1
-rw-r--r--testing/web-platform/tests/webusb/requestDevice/sandboxed_iframe.https.window.js24
-rw-r--r--testing/web-platform/tests/webusb/resources/fake-devices.js175
-rw-r--r--testing/web-platform/tests/webusb/resources/manual.js110
-rw-r--r--testing/web-platform/tests/webusb/resources/open-in-iframe.html42
-rw-r--r--testing/web-platform/tests/webusb/resources/open-in-worker.js16
-rw-r--r--testing/web-platform/tests/webusb/resources/usb-allowed-by-permissions-policy-worker.js14
-rw-r--r--testing/web-platform/tests/webusb/resources/usb-disabled-by-permissions-policy-worker.js17
-rw-r--r--testing/web-platform/tests/webusb/resources/usb-helpers.js104
-rw-r--r--testing/web-platform/tests/webusb/usb-allowed-by-permissions-policy-attribute-redirect-on-load.https.sub.html44
-rw-r--r--testing/web-platform/tests/webusb/usb-allowed-by-permissions-policy-attribute.https.sub.html46
-rw-r--r--testing/web-platform/tests/webusb/usb-allowed-by-permissions-policy.https.sub.html46
-rw-r--r--testing/web-platform/tests/webusb/usb-allowed-by-permissions-policy.https.sub.html.headers1
-rw-r--r--testing/web-platform/tests/webusb/usb-default-permissions-policy.https.sub.html27
-rw-r--r--testing/web-platform/tests/webusb/usb-disabled-by-permissions-policy.https.sub.html57
-rw-r--r--testing/web-platform/tests/webusb/usb-disabled-by-permissions-policy.https.sub.html.headers1
-rw-r--r--testing/web-platform/tests/webusb/usb-garbage-collection.https.window.js15
-rw-r--r--testing/web-platform/tests/webusb/usb-supported-by-permissions-policy.html11
-rw-r--r--testing/web-platform/tests/webusb/usb.https.any.js50
-rw-r--r--testing/web-platform/tests/webusb/usb.https.window.js129
-rw-r--r--testing/web-platform/tests/webusb/usb.serviceworker.https.html12
-rw-r--r--testing/web-platform/tests/webusb/usb.serviceworker.js9
-rw-r--r--testing/web-platform/tests/webusb/usbAlternateInterface.https.any.js34
-rw-r--r--testing/web-platform/tests/webusb/usbConfiguration.https.any.js24
-rw-r--r--testing/web-platform/tests/webusb/usbConnectionEvent.https.any.js22
-rw-r--r--testing/web-platform/tests/webusb/usbDevice-iframe.https.html86
-rw-r--r--testing/web-platform/tests/webusb/usbDevice-same-objecct.https.any.js26
-rw-r--r--testing/web-platform/tests/webusb/usbDevice-worker.https.html36
-rw-r--r--testing/web-platform/tests/webusb/usbDevice.https.any.js1249
-rw-r--r--testing/web-platform/tests/webusb/usbDevice_claimInterface-manual.https.html46
-rw-r--r--testing/web-platform/tests/webusb/usbDevice_controlTransferIn-manual.https.html348
-rw-r--r--testing/web-platform/tests/webusb/usbDevice_forget-manual.https.html27
-rw-r--r--testing/web-platform/tests/webusb/usbDevice_reset-manual.https.html46
-rw-r--r--testing/web-platform/tests/webusb/usbDevice_transferIn-manual.https.html148
-rw-r--r--testing/web-platform/tests/webusb/usbEndpoint.https.any.js46
-rw-r--r--testing/web-platform/tests/webusb/usbInTransferResult.https.any.js29
-rw-r--r--testing/web-platform/tests/webusb/usbInterface.https.any.js55
-rw-r--r--testing/web-platform/tests/webusb/usbIsochronousInTransferPacket.https.any.js28
-rw-r--r--testing/web-platform/tests/webusb/usbIsochronousInTransferResult.https.any.js36
-rw-r--r--testing/web-platform/tests/webusb/usbIsochronousOutTransferPacket.https.any.js21
-rw-r--r--testing/web-platform/tests/webusb/usbIsochronousOutTransferResult.https.any.js19
-rw-r--r--testing/web-platform/tests/webusb/usbOutTransferResult.https.any.js19
51 files changed, 3522 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webusb/META.yml b/testing/web-platform/tests/webusb/META.yml
new file mode 100644
index 0000000000..546094855e
--- /dev/null
+++ b/testing/web-platform/tests/webusb/META.yml
@@ -0,0 +1,3 @@
+spec: https://wicg.github.io/webusb/
+suggested_reviewers:
+ - reillyeon
diff --git a/testing/web-platform/tests/webusb/README.md b/testing/web-platform/tests/webusb/README.md
new file mode 100644
index 0000000000..c19e8fa347
--- /dev/null
+++ b/testing/web-platform/tests/webusb/README.md
@@ -0,0 +1,17 @@
+# WebUSB Testing
+
+WebUSB testing relies on the [WebUSB Testing API] which must be
+provided by browsers under test.
+
+In this test suite `resources/usb-helpers.js` detects and triggers
+the API to be loaded as needed.
+
+The Chromium implementation is provided by
+`../resources/chromium/webusb-test.js` using [MojoJS].
+
+Tests with the "-manual" suffix do not use the test-only interface and expect a
+real hardware device to be connected. The specific characteristics of the device
+are described in each test.
+
+[MojoJS]: https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/testing/web_platform_tests.md#mojojs
+[WebUSB Testing API]: https://wicg.github.io/webusb/test/
diff --git a/testing/web-platform/tests/webusb/getDevices/reject_opaque_origin.https.html b/testing/web-platform/tests/webusb/getDevices/reject_opaque_origin.https.html
new file mode 100644
index 0000000000..7cb503ce3c
--- /dev/null
+++ b/testing/web-platform/tests/webusb/getDevices/reject_opaque_origin.https.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ 'use strict';
+
+ promise_test(async (t) => {
+ await promise_rejects_dom(
+ t, 'SecurityError', navigator.usb.getDevices(),
+ 'getDevices() should throw a SecurityError DOMException when called ' +
+ 'from a context where the top-level document has an opaque origin.');
+ }, 'Calls to USB APIs from an origin with opaque top origin get blocked.');
+</script>
diff --git a/testing/web-platform/tests/webusb/getDevices/reject_opaque_origin.https.html.headers b/testing/web-platform/tests/webusb/getDevices/reject_opaque_origin.https.html.headers
new file mode 100644
index 0000000000..1efcf8c226
--- /dev/null
+++ b/testing/web-platform/tests/webusb/getDevices/reject_opaque_origin.https.html.headers
@@ -0,0 +1 @@
+Content-Security-Policy: sandbox allow-scripts
diff --git a/testing/web-platform/tests/webusb/getDevices/sandboxed_iframe.https.window.js b/testing/web-platform/tests/webusb/getDevices/sandboxed_iframe.https.window.js
new file mode 100644
index 0000000000..60bdf30587
--- /dev/null
+++ b/testing/web-platform/tests/webusb/getDevices/sandboxed_iframe.https.window.js
@@ -0,0 +1,19 @@
+'use strict';
+
+promise_test(async (t) => {
+ let iframe = document.createElement('iframe');
+ await new Promise(resolve => {
+ iframe.src = '../resources/open-in-iframe.html';
+ iframe.sandbox.add('allow-scripts');
+ iframe.allow = 'usb';
+ document.body.appendChild(iframe);
+ iframe.addEventListener('load', resolve);
+ });
+ await new Promise(resolve => {
+ window.addEventListener('message', t.step_func(messageEvent => {
+ assert_equals(messageEvent.data, 'Success');
+ resolve();
+ }));
+ iframe.contentWindow.postMessage('GetDevices', '*');
+ });
+}, 'GetDevices from a sandboxed iframe is valid.');
diff --git a/testing/web-platform/tests/webusb/idlharness.https.any.js b/testing/web-platform/tests/webusb/idlharness.https.any.js
new file mode 100644
index 0000000000..0c8cb322a0
--- /dev/null
+++ b/testing/web-platform/tests/webusb/idlharness.https.any.js
@@ -0,0 +1,47 @@
+// META: timeout=long
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+// META: script=/resources/test-only-api.js
+// META: script=/webusb/resources/fake-devices.js
+// META: script=/webusb/resources/usb-helpers.js
+
+'use strict';
+
+idl_test(
+ ['webusb'],
+ ['permissions', 'html', 'dom'],
+ async idl_array => {
+ if (self.GLOBAL.isWindow()) {
+ idl_array.add_objects({ Navigator: ['navigator'] });
+ } else if (self.GLOBAL.isWorker()) {
+ idl_array.add_objects({ WorkerNavigator: ['navigator'] });
+ }
+
+ idl_array.add_objects({
+ USB: ['navigator.usb'],
+ USBAlternateInterface: ['usbAlternateInterface'],
+ USBConfiguration: ['usbConfiguration'],
+ USBConnectionEvent: ['usbConnectionEvent'],
+ USBDevice: ['usbDevice'],
+ USBEndpoint: ['usbEndpoint'],
+ USBInterface: ['usbInterface'],
+ USBInTransferResult: ['new USBInTransferResult("ok")'],
+ USBOutTransferResult: ['new USBOutTransferResult("ok")'],
+ USBIsochronousInTransferResult: ['new USBIsochronousInTransferResult([])'],
+ USBIsochronousOutTransferResult: ['new USBIsochronousOutTransferResult([])'],
+ USBIsochronousInTransferPacket: ['new USBIsochronousInTransferPacket("ok")'],
+ USBIsochronousOutTransferPacket: ['new USBIsochronousOutTransferPacket("ok")'],
+ });
+
+ return usb_test(async () => {
+ // Ignored errors are surfaced in idlharness.js's test_object below.
+ self.usbDevice = await getFakeDevice().device;
+ self.usbConfiguration = usbDevice.configurations[0];
+ self.usbInterface = usbConfiguration.interfaces[0];
+ self.usbAlternateInterface = usbInterface.alternates[0];
+ self.usbEndpoint = usbAlternateInterface.endpoints[0];
+ self.usbConnectionEvent =
+ new USBConnectionEvent('connect', { device: usbDevice });
+ }, 'USB device setup');
+ }
+);
diff --git a/testing/web-platform/tests/webusb/insecure-context.any.js b/testing/web-platform/tests/webusb/insecure-context.any.js
new file mode 100644
index 0000000000..962738987b
--- /dev/null
+++ b/testing/web-platform/tests/webusb/insecure-context.any.js
@@ -0,0 +1,21 @@
+'use strict';
+
+test(() => {
+ assert_false(isSecureContext);
+ assert_false('usb' in navigator);
+}, '"usb" should not be present on navigator in an insecure context.');
+
+[
+ 'USB', 'USBAlternateInterface', 'USBConfiguration', 'USBConnectionEvent',
+ 'USBDevice', 'USBEndpoint', 'USBInterface', 'USBInTransferResult',
+ 'USBOutTransferResult', 'USBIsochronousInTransferResult',
+ 'USBIsochronousOutTransferResult', 'USBIsochronousInTransferPacket',
+ 'USBIsochronousOutTransferPacket',
+].forEach((symbol) => {
+ test(() => {
+ assert_false(isSecureContext);
+ assert_false(symbol in this)
+ }, '"' + symbol + '" should not be visible in an insecure context.');
+});
+
+done();
diff --git a/testing/web-platform/tests/webusb/protected-interface-classes.https.any.js b/testing/web-platform/tests/webusb/protected-interface-classes.https.any.js
new file mode 100644
index 0000000000..027c2c418c
--- /dev/null
+++ b/testing/web-platform/tests/webusb/protected-interface-classes.https.any.js
@@ -0,0 +1,89 @@
+// META: script=/resources/test-only-api.js
+// META: script=/webusb/resources/usb-helpers.js
+'use strict';
+
+async function runTestForInterfaceClass(t, interfaceClass) {
+ await navigator.usb.test.initialize();
+
+ const fakeDeviceTemplate = {
+ usbVersionMajor: 2,
+ usbVersionMinor: 0,
+ usbVersionSubminor: 0,
+ deviceClass: 7,
+ deviceSubclass: 1,
+ deviceProtocol: 2,
+ vendorId: 0x18d1,
+ productId: 0xf00d,
+ deviceVersionMajor: 1,
+ deviceVersionMinor: 2,
+ deviceVersionSubminor: 3,
+ manufacturerName: 'Google, Inc.',
+ productName: 'Test Device',
+ serialNumber: '4 (chosen randomly)',
+ activeConfigurationValue: 0,
+ configurations: [{
+ configurationValue: 1,
+ configurationName: 'Default configuration',
+ interfaces: [{
+ interfaceNumber: 0,
+ alternates: [{
+ alternateSetting: 0,
+ interfaceClass: interfaceClass,
+ interfaceSubclass: 0x01,
+ interfaceProtocol: 0x01,
+ interfaceName: 'Protected interface',
+ endpoints: []
+ }]
+ }, {
+ interfaceNumber: 1,
+ alternates: [{
+ alternateSetting: 0,
+ interfaceClass: 0xff,
+ interfaceSubclass: 0x01,
+ interfaceProtocol: 0x01,
+ interfaceName: 'Unprotected interface',
+ endpoints: []
+ }]
+ }]
+ }]
+ };
+
+ let fakeDevice;
+ let device = await new Promise((resolve) => {
+ navigator.usb.addEventListener('connect', (e) => {
+ resolve(e.device);
+ }, { once: true });
+ fakeDevice = navigator.usb.test.addFakeDevice(fakeDeviceTemplate);
+ });
+
+ await device.open();
+ await device.selectConfiguration(1);
+
+ await promise_rejects_dom(t, 'SecurityError', device.claimInterface(0));
+ await device.claimInterface(1);
+
+ await device.close();
+ fakeDevice.disconnect();
+}
+
+usb_test(
+ (t) => runTestForInterfaceClass(t, 0x01),
+ 'Protected audio interface cannot be claimed');
+usb_test(
+ (t) => runTestForInterfaceClass(t, 0x03),
+ 'Protected HID interface cannot be claimed');
+usb_test(
+ (t) => runTestForInterfaceClass(t, 0x08),
+ 'Protected mass storage interface cannot be claimed');
+usb_test(
+ (t) => runTestForInterfaceClass(t, 0x0B),
+ 'Protected smart card interface cannot be claimed');
+usb_test(
+ (t) => runTestForInterfaceClass(t, 0x0E),
+ 'Protected video interface cannot be claimed');
+usb_test(
+ (t) => runTestForInterfaceClass(t, 0x10),
+ 'Protected audio/video interface cannot be claimed');
+usb_test(
+ (t) => runTestForInterfaceClass(t, 0xE0),
+ 'Protected wireless controller interface cannot be claimed');
diff --git a/testing/web-platform/tests/webusb/requestDevice/reject_opaque_origin.https.html b/testing/web-platform/tests/webusb/requestDevice/reject_opaque_origin.https.html
new file mode 100644
index 0000000000..34798ce2b1
--- /dev/null
+++ b/testing/web-platform/tests/webusb/requestDevice/reject_opaque_origin.https.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ 'use strict';
+
+ promise_test(async (t) => {
+ await promise_rejects_dom(
+ t, 'SecurityError', navigator.usb.requestDevice({filters:[]}),
+ 'requestDevice() should throw a SecurityError DOMException when ' +
+ 'called from a context where the top-level document has an opaque ' +
+ 'origin.');
+ }, 'Calls to USB APIs from an origin with opaque top origin get blocked.');
+</script>
diff --git a/testing/web-platform/tests/webusb/requestDevice/reject_opaque_origin.https.html.headers b/testing/web-platform/tests/webusb/requestDevice/reject_opaque_origin.https.html.headers
new file mode 100644
index 0000000000..1efcf8c226
--- /dev/null
+++ b/testing/web-platform/tests/webusb/requestDevice/reject_opaque_origin.https.html.headers
@@ -0,0 +1 @@
+Content-Security-Policy: sandbox allow-scripts
diff --git a/testing/web-platform/tests/webusb/requestDevice/sandboxed_iframe.https.window.js b/testing/web-platform/tests/webusb/requestDevice/sandboxed_iframe.https.window.js
new file mode 100644
index 0000000000..b63f409480
--- /dev/null
+++ b/testing/web-platform/tests/webusb/requestDevice/sandboxed_iframe.https.window.js
@@ -0,0 +1,24 @@
+'use strict';
+
+promise_test(async (t) => {
+ let iframe = document.createElement('iframe');
+ await new Promise(resolve => {
+ iframe.src = '../resources/open-in-iframe.html';
+ iframe.sandbox.add('allow-scripts');
+ iframe.allow = 'usb';
+ document.body.appendChild(iframe);
+ iframe.addEventListener('load', resolve);
+ });
+ await new Promise(resolve => {
+ window.addEventListener('message', t.step_func(messageEvent => {
+ // The failure message of no device chosen is expected. The point here is
+ // to validate not failing because of a sandboxed iframe.
+ assert_equals(
+ 'FAIL: NotFoundError: Failed to execute \'requestDevice\' on ' +
+ '\'USB\': No device selected.',
+ messageEvent.data);
+ resolve();
+ }));
+ iframe.contentWindow.postMessage('RequestDevice', '*');
+ });
+}, 'RequestDevice from a sandboxed iframe is valid.');
diff --git a/testing/web-platform/tests/webusb/resources/fake-devices.js b/testing/web-platform/tests/webusb/resources/fake-devices.js
new file mode 100644
index 0000000000..c5c5cadaa6
--- /dev/null
+++ b/testing/web-platform/tests/webusb/resources/fake-devices.js
@@ -0,0 +1,175 @@
+'use strict';
+
+let fakeDeviceInit = {
+ usbVersionMajor: 2,
+ usbVersionMinor: 0,
+ usbVersionSubminor: 0,
+ deviceClass: 7,
+ deviceSubclass: 1,
+ deviceProtocol: 2,
+ vendorId: 0x18d1,
+ productId: 0xf00d,
+ deviceVersionMajor: 1,
+ deviceVersionMinor: 2,
+ deviceVersionSubminor: 3,
+ manufacturerName: 'Google, Inc.',
+ productName: 'The amazing imaginary printer',
+ serialNumber: '4',
+ activeConfigurationValue: 0,
+ configurations: [
+ {
+ configurationValue: 1,
+ configurationName: 'Printer Mode',
+ interfaces: [
+ {
+ interfaceNumber: 0,
+ alternates: [{
+ alternateSetting: 0,
+ interfaceClass: 0xff,
+ interfaceSubclass: 0x01,
+ interfaceProtocol: 0x01,
+ interfaceName: 'Control',
+ endpoints: [{
+ endpointNumber: 1,
+ direction: 'in',
+ type: 'interrupt',
+ packetSize: 8
+ }]
+ }]
+ },
+ {
+ interfaceNumber: 1,
+ alternates: [{
+ alternateSetting: 0,
+ interfaceClass: 0xff,
+ interfaceSubclass: 0x02,
+ interfaceProtocol: 0x01,
+ interfaceName: 'Data',
+ endpoints: [
+ {
+ endpointNumber: 2,
+ direction: 'in',
+ type: 'bulk',
+ packetSize: 1024
+ },
+ {
+ endpointNumber: 2,
+ direction: 'out',
+ type: 'bulk',
+ packetSize: 1024
+ }
+ ]
+ }]
+ }
+ ]
+ },
+ {
+ configurationValue: 2,
+ configurationName: 'Fighting Robot Mode',
+ interfaces: [{
+ interfaceNumber: 0,
+ alternates: [
+ {
+ alternateSetting: 0,
+ interfaceClass: 0xff,
+ interfaceSubclass: 0x42,
+ interfaceProtocol: 0x01,
+ interfaceName: 'Disabled',
+ endpoints: []
+ },
+ {
+ alternateSetting: 1,
+ interfaceClass: 0xff,
+ interfaceSubclass: 0x42,
+ interfaceProtocol: 0x01,
+ interfaceName: 'Activate!',
+ endpoints: [
+ {
+ endpointNumber: 1,
+ direction: 'in',
+ type: 'isochronous',
+ packetSize: 1024
+ },
+ {
+ endpointNumber: 1,
+ direction: 'out',
+ type: 'isochronous',
+ packetSize: 1024
+ }
+ ]
+ }
+ ]
+ }]
+ },
+ {
+ configurationValue: 3,
+ configurationName: 'Non-sequential interface number and alternate ' +
+ 'setting Mode',
+ interfaces: [
+ {
+ interfaceNumber: 0,
+ alternates: [
+ {
+ alternateSetting: 0,
+ interfaceClass: 0xff,
+ interfaceSubclass: 0x01,
+ interfaceProtocol: 0x01,
+ interfaceName: 'Control',
+ endpoints: [{
+ endpointNumber: 1,
+ direction: 'in',
+ type: 'interrupt',
+ packetSize: 8
+ }]
+ },
+ {
+ alternateSetting: 2,
+ interfaceClass: 0xff,
+ interfaceSubclass: 0x02,
+ interfaceProtocol: 0x01,
+ interfaceName: 'Data',
+ endpoints: [
+ {
+ endpointNumber: 2,
+ direction: 'in',
+ type: 'bulk',
+ packetSize: 1024
+ },
+ {
+ endpointNumber: 2,
+ direction: 'out',
+ type: 'bulk',
+ packetSize: 1024
+ }
+ ]
+ }
+ ]
+ },
+ {
+ interfaceNumber: 2,
+ alternates: [{
+ alternateSetting: 0,
+ interfaceClass: 0xff,
+ interfaceSubclass: 0x02,
+ interfaceProtocol: 0x01,
+ interfaceName: 'Data',
+ endpoints: [
+ {
+ endpointNumber: 2,
+ direction: 'in',
+ type: 'bulk',
+ packetSize: 1024
+ },
+ {
+ endpointNumber: 2,
+ direction: 'out',
+ type: 'bulk',
+ packetSize: 1024
+ }
+ ]
+ }]
+ }
+ ]
+ }
+ ]
+};
diff --git a/testing/web-platform/tests/webusb/resources/manual.js b/testing/web-platform/tests/webusb/resources/manual.js
new file mode 100644
index 0000000000..e8dc08a8bd
--- /dev/null
+++ b/testing/web-platform/tests/webusb/resources/manual.js
@@ -0,0 +1,110 @@
+let manualTestDevice = null;
+
+navigator.usb.addEventListener('disconnect', (e) => {
+ if (e.device === manualTestDevice) {
+ manualTestDevice = null;
+ }
+})
+
+async function getDeviceForManualTest() {
+ if (manualTestDevice) {
+ return manualTestDevice;
+ }
+
+ const button = document.createElement('button');
+ button.textContent = 'Click to select a device';
+ button.style.display = 'block';
+ button.style.fontSize = '20px';
+ button.style.padding = '10px';
+
+ await new Promise((resolve) => {
+ button.onclick = () => {
+ document.body.removeChild(button);
+ resolve();
+ };
+ document.body.appendChild(button);
+ });
+
+ manualTestDevice = await navigator.usb.requestDevice({filters: []});
+ assert_true(manualTestDevice instanceof USBDevice);
+
+ return manualTestDevice;
+}
+
+function manual_usb_test(func, name, properties) {
+ promise_test(async (test) => {
+ await func(test, await getDeviceForManualTest());
+ }, name, properties);
+}
+
+function manual_usb_serial_test(func, name, properties) {
+ promise_test(async (test) => {
+ const device = await getDeviceForManualTest();
+ await device.open();
+ test.add_cleanup(async () => {
+ if (device.opened) {
+ await device.close();
+ }
+ });
+
+ await device.selectConfiguration(1);
+
+ let controlInterface = undefined;
+ for (const iface of device.configuration.interfaces) {
+ const alternate = iface.alternates[0];
+ if (alternate.interfaceClass == 2 &&
+ alternate.interfaceSubclass == 2 &&
+ alternate.interfaceProtocol == 0) {
+ controlInterface = iface;
+ break;
+ }
+ }
+ assert_not_equals(controlInterface, undefined,
+ 'No control interface found.');
+
+ let dataInterface = undefined;
+ for (const iface of device.configuration.interfaces) {
+ const alternate = iface.alternates[0];
+ if (alternate.interfaceClass == 10 &&
+ alternate.interfaceSubclass == 0 &&
+ alternate.interfaceProtocol == 0) {
+ dataInterface = iface;
+ break;
+ }
+ }
+ assert_not_equals(dataInterface, undefined, 'No data interface found.');
+
+ await device.claimInterface(controlInterface.interfaceNumber);
+ await device.claimInterface(dataInterface.interfaceNumber);
+
+ let inEndpoint = undefined;
+ for (const endpoint of dataInterface.alternate.endpoints) {
+ if (endpoint.type == 'bulk' && endpoint.direction == 'in') {
+ inEndpoint = endpoint;
+ break;
+ }
+ }
+ assert_not_equals(inEndpoint, undefined, 'No IN endpoint found.');
+
+ let outEndpoint = undefined;
+ for (const endpoint of dataInterface.alternate.endpoints) {
+ if (endpoint.type == 'bulk' && endpoint.direction == 'out') {
+ outEndpoint = endpoint;
+ break;
+ }
+ }
+ assert_not_equals(outEndpoint, undefined, 'No OUT endpoint found.');
+
+ // Execute a SET_CONTROL_LINE_STATE command to let the device know the
+ // host is ready to transmit and receive data.
+ await device.controlTransferOut({
+ requestType: 'class',
+ recipient: 'interface',
+ request: 0x22,
+ value: 0x01,
+ index: controlInterface.interfaceNumber,
+ });
+
+ await func(test, device, inEndpoint, outEndpoint);
+ }, name, properties);
+} \ No newline at end of file
diff --git a/testing/web-platform/tests/webusb/resources/open-in-iframe.html b/testing/web-platform/tests/webusb/resources/open-in-iframe.html
new file mode 100644
index 0000000000..ad0e12d371
--- /dev/null
+++ b/testing/web-platform/tests/webusb/resources/open-in-iframe.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<!-- Intentionally use relative paths here because this file is also used by blink/web_tests/usb/usbDevice-iframe.html. -->
+<script src="../../resources/test-only-api.js"></script>
+<script src="usb-helpers.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<body>
+ <button>Fake user gesture</button>
+</body>
+
+<script>
+ 'use strict';
+ test_driver.set_test_context(parent);
+ window.onmessage = messageEvent => {
+ switch (messageEvent.data) {
+ case 'ConnectEvent':
+ navigator.usb.addEventListener('connect', connectEvent => {
+ connectEvent.device.open()
+ .then(() => parent.postMessage('Success', '*'))
+ .catch(err =>
+ parent.postMessage(`FAIL: open rejected ${err}`, '*'));
+ });
+ parent.postMessage('Ready', '*');
+ break;
+ case 'GetDevices':
+ navigator.usb.getDevices()
+ .then(devices => parent.postMessage('Success', '*'))
+ .catch(err => parent.postMessage(`FAIL: ${err}`, '*'));
+ break;
+ case 'RequestDevice':
+ test_driver.click(document.getElementsByTagName('button')[0])
+ .then(() => navigator.usb.requestDevice({filters: []}))
+ .then(device => parent.postMessage('Success', '*'))
+ .catch(err => parent.postMessage(`FAIL: ${err}`, '*'));
+ break;
+ default:
+ parent.postMessage(
+ `FAIL: Bad message type: ${messageEvent.data}`, '*');
+ }
+ };
+</script>
diff --git a/testing/web-platform/tests/webusb/resources/open-in-worker.js b/testing/web-platform/tests/webusb/resources/open-in-worker.js
new file mode 100644
index 0000000000..2175cfd397
--- /dev/null
+++ b/testing/web-platform/tests/webusb/resources/open-in-worker.js
@@ -0,0 +1,16 @@
+importScripts('/resources/test-only-api.js');
+importScripts('/webusb/resources/usb-helpers.js');
+'use strict';
+
+onmessage = messageEvent => {
+ if (messageEvent.data.type === 'ConnectEvent') {
+ navigator.usb.addEventListener('connect', connectEvent => {
+ connectEvent.device.open().then(() => {
+ postMessage({ type: 'Success' });
+ }).catch(error => {
+ postMessage({ type: `FAIL: open rejected ${error}` });
+ });
+ });
+ postMessage({ type: 'Ready' });
+ }
+};
diff --git a/testing/web-platform/tests/webusb/resources/usb-allowed-by-permissions-policy-worker.js b/testing/web-platform/tests/webusb/resources/usb-allowed-by-permissions-policy-worker.js
new file mode 100644
index 0000000000..d06a586474
--- /dev/null
+++ b/testing/web-platform/tests/webusb/resources/usb-allowed-by-permissions-policy-worker.js
@@ -0,0 +1,14 @@
+'use strict';
+
+importScripts('/resources/testharness.js');
+
+let workerType;
+
+if (typeof postMessage === 'function') {
+ workerType = 'dedicated';
+}
+
+promise_test(() => navigator.usb.getDevices(),
+ `Inherited header permissions policy allows ${workerType} workers.`);
+
+done();
diff --git a/testing/web-platform/tests/webusb/resources/usb-disabled-by-permissions-policy-worker.js b/testing/web-platform/tests/webusb/resources/usb-disabled-by-permissions-policy-worker.js
new file mode 100644
index 0000000000..caf2727cd1
--- /dev/null
+++ b/testing/web-platform/tests/webusb/resources/usb-disabled-by-permissions-policy-worker.js
@@ -0,0 +1,17 @@
+'use strict';
+
+importScripts('/resources/testharness.js');
+
+const header = 'Permissions-Policy header usb=()';
+let workerType;
+
+if (typeof postMessage === 'function') {
+ workerType = 'dedicated';
+}
+
+promise_test(() => navigator.usb.getDevices().then(
+ () => assert_unreached('expected promise to reject with SecurityError'),
+ error => assert_equals(error.name, 'SecurityError')),
+ `Inherited ${header} disallows ${workerType} workers.`);
+
+done();
diff --git a/testing/web-platform/tests/webusb/resources/usb-helpers.js b/testing/web-platform/tests/webusb/resources/usb-helpers.js
new file mode 100644
index 0000000000..cb6aaadf98
--- /dev/null
+++ b/testing/web-platform/tests/webusb/resources/usb-helpers.js
@@ -0,0 +1,104 @@
+'use strict';
+
+// These tests rely on the User Agent providing an implementation of the
+// WebUSB Testing API (https://wicg.github.io/webusb/test/).
+//
+// In Chromium-based browsers this implementation is provided by a polyfill
+// in order to reduce the amount of test-only code shipped to users. To enable
+// these tests the browser must be run with these options:
+//
+// --enable-blink-features=MojoJS,MojoJSTest
+
+(() => {
+ // Load scripts needed by the test API on context creation.
+ if (isChromiumBased) {
+ loadScript('/resources/chromium/webusb-child-test.js');
+ }
+})();
+
+function usb_test(func, name, properties) {
+ promise_test(async (t) => {
+ assert_implements(navigator.usb, 'missing navigator.usb');
+ if (navigator.usb.test === undefined) {
+ // Try loading a polyfill for the WebUSB Testing API.
+ if (isChromiumBased) {
+ await loadScript('/resources/chromium/webusb-test.js');
+ }
+ }
+ assert_implements(navigator.usb.test, 'missing navigator.usb.test after initialization');
+
+ await navigator.usb.test.initialize();
+ try {
+ await func(t);
+ } finally {
+ await navigator.usb.test.reset();
+ }
+ }, name, properties);
+}
+
+// Returns a promise that is resolved when the next USBConnectionEvent of the
+// given type is received.
+function connectionEventPromise(eventType) {
+ return new Promise(resolve => {
+ let eventHandler = e => {
+ assert_true(e instanceof USBConnectionEvent);
+ navigator.usb.removeEventListener(eventType, eventHandler);
+ resolve(e.device);
+ };
+ navigator.usb.addEventListener(eventType, eventHandler);
+ });
+}
+
+// Creates a fake device and returns a promise that resolves once the
+// 'connect' event is fired for the fake device. The promise is resolved with
+// an object containing the fake USB device and the corresponding USBDevice.
+function getFakeDevice() {
+ let promise = connectionEventPromise('connect');
+ let fakeDevice = navigator.usb.test.addFakeDevice(fakeDeviceInit);
+ return promise.then(device => {
+ return { device: device, fakeDevice: fakeDevice };
+ });
+}
+
+// Disconnects the given device and returns a promise that is resolved when it
+// is done.
+function waitForDisconnect(fakeDevice) {
+ let promise = connectionEventPromise('disconnect');
+ fakeDevice.disconnect();
+ return promise;
+}
+
+function assertDeviceInfoEquals(usbDevice, deviceInit) {
+ for (var property in deviceInit) {
+ if (property == 'activeConfigurationValue') {
+ if (deviceInit.activeConfigurationValue == 0) {
+ assert_equals(usbDevice.configuration, null);
+ } else {
+ assert_equals(usbDevice.configuration.configurationValue,
+ deviceInit.activeConfigurationValue);
+ }
+ } else if (Array.isArray(deviceInit[property])) {
+ assert_equals(usbDevice[property].length, deviceInit[property].length);
+ for (var i = 0; i < usbDevice[property].length; ++i)
+ assertDeviceInfoEquals(usbDevice[property][i], deviceInit[property][i]);
+ } else {
+ assert_equals(usbDevice[property], deviceInit[property], property);
+ }
+ }
+}
+
+function callWithTrustedClick(callback) {
+ return new Promise(resolve => {
+ let button = document.createElement('button');
+ button.textContent = 'click to continue test';
+ button.style.display = 'block';
+ button.style.fontSize = '20px';
+ button.style.padding = '10px';
+ button.onclick = () => {
+ resolve(callback());
+ document.body.removeChild(button);
+ };
+ document.body.appendChild(button);
+ test_driver.click(button);
+ });
+}
diff --git a/testing/web-platform/tests/webusb/usb-allowed-by-permissions-policy-attribute-redirect-on-load.https.sub.html b/testing/web-platform/tests/webusb/usb-allowed-by-permissions-policy-attribute-redirect-on-load.https.sub.html
new file mode 100644
index 0000000000..013efd9b4d
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usb-allowed-by-permissions-policy-attribute-redirect-on-load.https.sub.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<body>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/permissions-policy/resources/permissions-policy.js></script>
+<script>
+'use strict';
+const relative_path = '/permissions-policy/resources/permissions-policy-usb.html';
+const base_src = '/permissions-policy/resources/redirect-on-load.html#';
+const relative_worker_frame_path =
+ '/permissions-policy/resources/permissions-policy-usb-worker.html';
+const sub = 'https://{{domains[www]}}:{{ports[https][0]}}';
+const same_origin_src = base_src + relative_path;
+const cross_origin_src = base_src + sub + relative_path;
+const same_origin_worker_frame_src = base_src + relative_worker_frame_path;
+const cross_origin_worker_frame_src = base_src + sub +
+ relative_worker_frame_path;
+const header = 'Permissions-Policy allow="usb"';
+
+async_test(t => {
+ test_feature_availability(
+ 'usb.getDevices()', t, same_origin_src,
+ expect_feature_available_default, 'usb');
+}, header + ' allows same-origin relocation.');
+
+async_test(t => {
+ test_feature_availability(
+ 'usb.getDevices()', t, same_origin_worker_frame_src,
+ expect_feature_available_default, 'usb');
+}, header + ' allows workers in same-origin relocation.');
+
+async_test(t => {
+ test_feature_availability(
+ 'usb.getDevices()', t, cross_origin_src,
+ expect_feature_unavailable_default, 'usb');
+}, header + ' disallows cross-origin relocation.');
+
+async_test(t => {
+ test_feature_availability(
+ 'usb.getDevices()', t, cross_origin_worker_frame_src,
+ expect_feature_unavailable_default, 'usb');
+}, header + ' disallows workers in cross-origin relocation.');
+</script>
+</body>
diff --git a/testing/web-platform/tests/webusb/usb-allowed-by-permissions-policy-attribute.https.sub.html b/testing/web-platform/tests/webusb/usb-allowed-by-permissions-policy-attribute.https.sub.html
new file mode 100644
index 0000000000..54af693da0
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usb-allowed-by-permissions-policy-attribute.https.sub.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<body>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/permissions-policy/resources/permissions-policy.js></script>
+<script>
+'use strict';
+const sub = 'https://{{domains[www]}}:{{ports[https][0]}}';
+const same_origin_src = '/permissions-policy/resources/permissions-policy-usb.html';
+const cross_origin_src = sub + same_origin_src;
+const same_origin_worker_frame_src =
+ '/permissions-policy/resources/permissions-policy-usb-worker.html';
+const cross_origin_worker_frame_src = sub + same_origin_worker_frame_src;
+const feature_name = 'Permissions policy "usb"';
+const header = 'allow="usb" attribute';
+
+async_test(t => {
+ test_feature_availability(
+ 'usb.getDevices()', t, same_origin_src,
+ expect_feature_available_default, 'usb');
+}, feature_name + ' can be enabled in same-origin iframe using ' + header);
+
+async_test(t => {
+ test_feature_availability(
+ 'usb.getDevices()', t, same_origin_worker_frame_src,
+ expect_feature_available_default, 'usb');
+}, feature_name + ' can be enabled in a worker in same-origin iframe using ' +
+ header);
+
+async_test(t => {
+ test_feature_availability(
+ 'usb.getDevices()', t, cross_origin_src,
+ expect_feature_available_default, 'usb');
+}, feature_name + ' can be enabled in cross-origin iframe using ' + header);
+
+async_test(t => {
+ test_feature_availability(
+ 'usb.getDevices()', t, cross_origin_worker_frame_src,
+ expect_feature_available_default, 'usb');
+}, feature_name + ' can be enabled in a worker in cross-origin iframe using ' +
+ header);
+
+fetch_tests_from_worker(new Worker(
+ '/webusb/resources/usb-allowed-by-permissions-policy-worker.js'));
+</script>
+</body>
diff --git a/testing/web-platform/tests/webusb/usb-allowed-by-permissions-policy.https.sub.html b/testing/web-platform/tests/webusb/usb-allowed-by-permissions-policy.https.sub.html
new file mode 100644
index 0000000000..e1461fe8e6
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usb-allowed-by-permissions-policy.https.sub.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<body>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/permissions-policy/resources/permissions-policy.js></script>
+<script>
+'use strict';
+const sub = 'https://{{domains[www]}}:{{ports[https][0]}}';
+const same_origin_src = '/permissions-policy/resources/permissions-policy-usb.html';
+const cross_origin_src = sub + same_origin_src;
+const same_origin_worker_frame_src =
+ '/permissions-policy/resources/permissions-policy-usb-worker.html';
+const cross_origin_worker_frame_src = sub + same_origin_worker_frame_src;
+const header = 'Permissions-Policy header usb=*';
+
+promise_test(
+ () => navigator.usb.getDevices(),
+ header + ' allows the top-level document.');
+
+async_test(t => {
+ test_feature_availability('usb.getDevices()', t, same_origin_src,
+ expect_feature_available_default);
+}, header + ' allows same-origin iframes.');
+
+async_test(t => {
+ test_feature_availability('usb.getDevices()', t, same_origin_worker_frame_src,
+ expect_feature_available_default);
+}, header + ' allows workers in same-origin iframes.');
+
+// Set allow="usb" on iframe element to delegate 'usb' to cross origin subframe.
+async_test(t => {
+ test_feature_availability('usb.getDevices()', t, cross_origin_src,
+ expect_feature_available_default, 'usb');
+}, header + ' allows cross-origin iframes.');
+
+// Set allow="usb" on iframe element to delegate 'usb' to cross origin subframe.
+async_test(t => {
+ test_feature_availability('usb.getDevices()', t,
+ cross_origin_worker_frame_src,
+ expect_feature_available_default, 'usb');
+}, header + ' allows workers in cross-origin iframes.');
+
+fetch_tests_from_worker(new Worker(
+ '/webusb/resources/usb-allowed-by-permissions-policy-worker.js'));
+</script>
+</body>
diff --git a/testing/web-platform/tests/webusb/usb-allowed-by-permissions-policy.https.sub.html.headers b/testing/web-platform/tests/webusb/usb-allowed-by-permissions-policy.https.sub.html.headers
new file mode 100644
index 0000000000..022b027812
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usb-allowed-by-permissions-policy.https.sub.html.headers
@@ -0,0 +1 @@
+Permissions-Policy: usb=*
diff --git a/testing/web-platform/tests/webusb/usb-default-permissions-policy.https.sub.html b/testing/web-platform/tests/webusb/usb-default-permissions-policy.https.sub.html
new file mode 100644
index 0000000000..5a9ddcb4fe
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usb-default-permissions-policy.https.sub.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<body>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src=/permissions-policy/resources/permissions-policy.js></script>
+<script>
+'use strict';
+var same_origin_src = '/permissions-policy/resources/permissions-policy-usb.html';
+var cross_origin_src = 'https://{{domains[www]}}:{{ports[https][0]}}' +
+ same_origin_src;
+var header = 'Default "usb" permissions policy ["self"]';
+
+promise_test(
+ () => navigator.usb.getDevices(),
+ header + ' allows the top-level document.');
+
+async_test(t => {
+ test_feature_availability('usb.getDevices()', t, same_origin_src,
+ expect_feature_available_default);
+}, header + ' allows same-origin iframes.');
+
+async_test(t => {
+ test_feature_availability('usb.getDevices()', t, cross_origin_src,
+ expect_feature_unavailable_default);
+}, header + ' disallows cross-origin iframes.');
+</script>
+</body>
diff --git a/testing/web-platform/tests/webusb/usb-disabled-by-permissions-policy.https.sub.html b/testing/web-platform/tests/webusb/usb-disabled-by-permissions-policy.https.sub.html
new file mode 100644
index 0000000000..3217d326f7
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usb-disabled-by-permissions-policy.https.sub.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<body>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/permissions-policy/resources/permissions-policy.js"></script>
+<script>
+'use strict';
+const sub = 'https://{{domains[www]}}:{{ports[https][0]}}';
+const same_origin_src = '/permissions-policy/resources/permissions-policy-usb.html';
+const cross_origin_src = sub + same_origin_src;
+const same_origin_worker_frame_src =
+ '/permissions-policy/resources/permissions-policy-usb-worker.html';
+const cross_origin_worker_frame_src = sub + same_origin_worker_frame_src;
+const header = 'Permissions-Policy header usb=()';
+
+promise_test(() => {
+ return navigator.usb.getDevices().then(() => {
+ assert_unreached('expected promise to reject with SecurityError');
+ }, error => {
+ assert_equals(error.name, 'SecurityError');
+ });
+}, header + ' disallows getDevices in the top-level document.');
+
+promise_test(async () => {
+ try {
+ await navigator.usb.requestDevice({ filters: [] });
+ assert_unreached('expected promise to reject with SecurityError');
+ } catch(error) {
+ assert_equals(error.name, 'SecurityError');
+ }
+}, header + ' disallows requestDevice in the top-level document.');
+
+async_test(t => {
+ test_feature_availability('usb.getDevices()', t, same_origin_src,
+ expect_feature_unavailable_default);
+}, header + ' disallows same-origin iframes.');
+
+async_test(t => {
+ test_feature_availability('usb.getDevices()', t, same_origin_worker_frame_src,
+ expect_feature_unavailable_default);
+}, header + ' disallows workers in same-origin iframes.');
+
+async_test(t => {
+ test_feature_availability('usb.getDevices()', t, cross_origin_src,
+ expect_feature_unavailable_default);
+}, header + ' disallows cross-origin iframes.');
+
+async_test(t => {
+ test_feature_availability('usb.getDevices()', t,
+ cross_origin_worker_frame_src,
+ expect_feature_unavailable_default);
+}, header + ' disallows workers in cross-origin iframes.');
+
+fetch_tests_from_worker(new Worker(
+ '/webusb/resources/usb-disabled-by-permissions-policy-worker.js'));
+</script>
+</body>
diff --git a/testing/web-platform/tests/webusb/usb-disabled-by-permissions-policy.https.sub.html.headers b/testing/web-platform/tests/webusb/usb-disabled-by-permissions-policy.https.sub.html.headers
new file mode 100644
index 0000000000..ff22d62f10
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usb-disabled-by-permissions-policy.https.sub.html.headers
@@ -0,0 +1 @@
+Permissions-Policy: usb=()
diff --git a/testing/web-platform/tests/webusb/usb-garbage-collection.https.window.js b/testing/web-platform/tests/webusb/usb-garbage-collection.https.window.js
new file mode 100644
index 0000000000..5c153eb0a9
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usb-garbage-collection.https.window.js
@@ -0,0 +1,15 @@
+// META: script=/resources/test-only-api.js
+// META: script=/webusb/resources/fake-devices.js
+// META: script=/webusb/resources/usb-helpers.js
+// META: script=/common/gc.js
+'use strict';
+
+usb_test(async () => {
+ {
+ let {device} = await getFakeDevice();
+ await device.open();
+ await device.selectConfiguration(2);
+ await device.claimInterface(0);
+ }
+ await garbageCollect();
+}, 'Run garbage collection when the device reference is out of scope');
diff --git a/testing/web-platform/tests/webusb/usb-supported-by-permissions-policy.html b/testing/web-platform/tests/webusb/usb-supported-by-permissions-policy.html
new file mode 100644
index 0000000000..8e6352116d
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usb-supported-by-permissions-policy.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<title>Test that usb is advertised in the feature list</title>
+<link rel="help" href="https://w3c.github.io/webappsec-permissions-policy/#dom-permissionspolicy-features">
+<link rel="help" href="https://wicg.github.io/webusb/#permissions-policy">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(() => {
+ assert_in_array('usb', document.featurePolicy.features());
+}, 'document.featurePolicy.features should advertise usb.');
+</script>
diff --git a/testing/web-platform/tests/webusb/usb.https.any.js b/testing/web-platform/tests/webusb/usb.https.any.js
new file mode 100644
index 0000000000..c9a95b10ad
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usb.https.any.js
@@ -0,0 +1,50 @@
+// META: script=/resources/test-only-api.js
+// META: script=/webusb/resources/fake-devices.js
+// META: script=/webusb/resources/usb-helpers.js
+'use strict';
+
+let usbDevice, devicesFirstTime, fakeDevice, removedDevice;
+
+usb_test(() => getFakeDevice()
+ .then(_ => usbDevice = _.device)
+ .then(() => navigator.usb.getDevices())
+ .then(devices => {
+ assert_equals(devices.length, 1);
+ assert_equals(usbDevice, devices[0]);
+ assertDeviceInfoEquals(devices[0], fakeDeviceInit);
+ }), 'getDevices returns devices that are connected');
+
+usb_test(() => getFakeDevice()
+ .then(() => navigator.usb.getDevices())
+ .then(_ => devicesFirstTime = _)
+ .then(() => assert_equals(devicesFirstTime.length, 1))
+ .then(() => navigator.usb.getDevices())
+ .then(devicesSecondTime => assert_array_equals(devicesSecondTime,
+ devicesFirstTime)),
+ 'getDevices returns the same objects for each USB device');
+
+usb_test(() => getFakeDevice()
+ .then(_ => usbDevice = _.device)
+ .then(() => assertDeviceInfoEquals(usbDevice, fakeDeviceInit))
+ .then(() => usbDevice.open())
+ .then(() => usbDevice.close()),
+ 'onconnect event is trigged by adding a device');
+
+usb_test(() => getFakeDevice()
+ .then(_ => {
+ usbDevice = _.device;
+ fakeDevice = _.fakeDevice;
+ })
+ .then(() => waitForDisconnect(fakeDevice))
+ .then(_ => removedDevice = _)
+ .then(() => {
+ assertDeviceInfoEquals(removedDevice, fakeDeviceInit);
+ assert_equals(removedDevice, usbDevice);
+ })
+ .then(() => removedDevice.open())
+ .then(() =>
+ assert_unreachable('should not be able to open a disconnected device'),
+ error => assert_equals(error.code, DOMException.NOT_FOUND_ERR)),
+ 'ondisconnect event is triggered by removing a device');
+
+done();
diff --git a/testing/web-platform/tests/webusb/usb.https.window.js b/testing/web-platform/tests/webusb/usb.https.window.js
new file mode 100644
index 0000000000..690faf3e92
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usb.https.window.js
@@ -0,0 +1,129 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/resources/test-only-api.js
+// META: script=/webusb/resources/fake-devices.js
+// META: script=/webusb/resources/usb-helpers.js
+'use strict';
+
+usb_test(() => {
+ return navigator.usb.requestDevice({ filters: [] })
+ .then(device => {
+ assert_unreachable('requestDevice should reject without a user gesture');
+ })
+ .catch(error => {
+ assert_equals(error.code, DOMException.SECURITY_ERR);
+ });
+}, 'requestDevice rejects when called without a user gesture');
+
+usb_test(() => {
+ return callWithTrustedClick(() => navigator.usb.requestDevice({ filters: [] })
+ .then(device => {
+ assert_unreachable('requestDevice should reject when no device selected');
+ })
+ .catch(error => {
+ assert_equals(error.code, DOMException.NOT_FOUND_ERR);
+ })
+ );
+}, 'requestDevice rejects when no device is chosen');
+
+usb_test(() => {
+ return getFakeDevice().then(({ device, fakeDevice }) => {
+ navigator.usb.test.onrequestdevice = event => {
+ navigator.usb.test.onrequestdevice = undefined;
+ event.respondWith(fakeDevice);
+ };
+ return callWithTrustedClick(() => {
+ return navigator.usb.requestDevice({ filters: [] }).then(chosenDevice => {
+ assert_equals(chosenDevice, device);
+ });
+ });
+ });
+}, 'requestDevice returns the device chosen by the user');
+
+usb_test(() => {
+ return getFakeDevice().then(({ device, fakeDevice }) => {
+ navigator.usb.test.onrequestdevice = event => {
+ navigator.usb.test.onrequestdevice = undefined;
+ event.respondWith(fakeDevice);
+ };
+ return callWithTrustedClick(() => {
+ return navigator.usb.requestDevice({ filters: [] }).then(chosenDevice => {
+ assert_equals(chosenDevice, device);
+ return navigator.usb.getDevices().then(devices => {
+ assert_equals(devices.length, 1);
+ assert_equals(devices[0], chosenDevice);
+ });
+ });
+ });
+ });
+}, 'getDevices returns the same object as requestDevice');
+
+usb_test(() => {
+ const expectedFilters = [
+ { vendorId: 1234, classCode: 0xFF, serialNumber: "123ABC" },
+ { vendorId: 5678, productId: 0xF00F },
+ { vendorId: 9012, classCode: 0xFF, subclassCode: 0xEE, protocolCode: 0xDD },
+ ];
+
+ navigator.usb.test.onrequestdevice = event => {
+ navigator.usb.test.onrequestdevice = undefined;
+
+ assert_equals(event.filters.length, expectedFilters.length);
+ for (var i = 0; i < event.filters.length; ++i) {
+ assert_object_equals(event.filters[i], expectedFilters[i]);
+ }
+
+ event.respondWith(null);
+ };
+
+ return callWithTrustedClick(() => {
+ return navigator.usb.requestDevice({ filters: expectedFilters })
+ .then(device => {
+ assert_unreached(
+ 'requestDevice should reject because no device selected');
+ })
+ .catch(error => {
+ assert_equals(error.code, DOMException.NOT_FOUND_ERR);
+ });
+ });
+}, 'filters are sent correctly');
+
+usb_test(async () => {
+ const badFilters = [
+ { productId: 1234 }, // productId requires vendorId
+ { subclassCode: 5678 }, // subclassCode requires classCode
+ { protocolCode: 9012 }, // protocolCode requires subclassCode
+ ];
+
+ for (const filter of badFilters) {
+ await callWithTrustedClick(async () => {
+ try {
+ await navigator.usb.requestDevice({ filters: [filter] });
+ assert_unreached(
+ 'requestDevice should reject because of invalid filters');
+ } catch (error) {
+ assert_equals(error.name, 'TypeError');
+ }
+ });
+ }
+}, 'requestDevice rejects on invalid filters');
+
+usb_test(() => {
+ return getFakeDevice().then(({ device, fakeDevice }) => {
+ navigator.usb.test.onrequestdevice = event => {
+ event.respondWith(fakeDevice);
+ };
+ return callWithTrustedClick(() => {
+ let first = navigator.usb.requestDevice({ filters: [] });
+ let second = navigator.usb.requestDevice({ filters: [] });
+ return Promise.all([
+ first.then(chosenDevice => {
+ assert_equals(chosenDevice, device);
+ }),
+ second.then(chosenDevice => {
+ assert_equals(chosenDevice, device);
+ })
+ ]);
+ });
+ });
+}, 'multiple requestDevice calls are allowed per user activation');
diff --git a/testing/web-platform/tests/webusb/usb.serviceworker.https.html b/testing/web-platform/tests/webusb/usb.serviceworker.https.html
new file mode 100644
index 0000000000..9a0b653a2a
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usb.serviceworker.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+
+service_worker_test('/webusb/usb.serviceworker.js',
+ 'Ensure that WebUSB is inaccessible from a service worker.');
+
+</script>
diff --git a/testing/web-platform/tests/webusb/usb.serviceworker.js b/testing/web-platform/tests/webusb/usb.serviceworker.js
new file mode 100644
index 0000000000..c509adfef0
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usb.serviceworker.js
@@ -0,0 +1,9 @@
+'use strict';
+importScripts('/resources/testharness.js');
+
+test(() => {
+ assert_equals(typeof navigator.usb, 'undefined',
+ 'navigator.usb should not be a USB object');
+}, 'Service workers should not have access to the WebUSB API.');
+
+done(); \ No newline at end of file
diff --git a/testing/web-platform/tests/webusb/usbAlternateInterface.https.any.js b/testing/web-platform/tests/webusb/usbAlternateInterface.https.any.js
new file mode 100644
index 0000000000..e97d53c16d
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usbAlternateInterface.https.any.js
@@ -0,0 +1,34 @@
+// META: script=/resources/test-only-api.js
+// META: script=/webusb/resources/fake-devices.js
+// META: script=/webusb/resources/usb-helpers.js
+'use strict';
+
+usb_test(async () => {
+ let { device } = await getFakeDevice();
+ let configuration = new USBConfiguration(
+ device, device.configurations[1].configurationValue);
+ let usbInterface = new USBInterface(
+ configuration, configuration.interfaces[0].interfaceNumber);
+ let alternateInterface = new USBAlternateInterface(
+ usbInterface, usbInterface.alternates[1].alternateSetting);
+ assertDeviceInfoEquals(
+ alternateInterface,
+ fakeDeviceInit.configurations[1].interfaces[0].alternates[1]);
+}, 'Can construct a USBAlternateInterface.');
+
+usb_test(async () => {
+ let { device } = await getFakeDevice();
+ let configuration = new USBConfiguration(
+ device, device.configurations[1].configurationValue);
+ let usbInterface = new USBInterface(
+ configuration, configuration.interfaces[0].interfaceNumber);
+ try {
+ let alternateInterface = new USBAlternateInterface(
+ usbInterface, usbInterface.alternates.length);
+ assert_unreached(
+ 'USBAlternateInterface should reject an invalid alternate setting');
+ } catch (error) {
+ assert_equals(error.name, 'RangeError');
+ }
+}, 'Constructing a USBAlternateInterface with an invalid alternate setting ' +
+ 'throws a range error.');
diff --git a/testing/web-platform/tests/webusb/usbConfiguration.https.any.js b/testing/web-platform/tests/webusb/usbConfiguration.https.any.js
new file mode 100644
index 0000000000..96aaee273d
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usbConfiguration.https.any.js
@@ -0,0 +1,24 @@
+// META: script=/resources/test-only-api.js
+// META: script=/webusb/resources/fake-devices.js
+// META: script=/webusb/resources/usb-helpers.js
+'use strict';
+
+usb_test(async () => {
+ let { device } = await getFakeDevice();
+ let configuration = new USBConfiguration(
+ device, device.configurations[1].configurationValue);
+ assertDeviceInfoEquals(configuration, fakeDeviceInit.configurations[1]);
+}, 'Can construct a USBConfiguration.');
+
+usb_test(async () => {
+ let { device } = await getFakeDevice();
+ try {
+ let configuration =
+ new USBConfiguration(device, device.configurations.length + 1);
+ assert_unreached(
+ 'USBConfiguration should reject an invalid configuration value');
+ } catch (error) {
+ assert_equals(error.name, 'RangeError');
+ }
+}, 'Constructing a USBConfiguration with an invalid configuration value ' +
+ 'throws a range error.');
diff --git a/testing/web-platform/tests/webusb/usbConnectionEvent.https.any.js b/testing/web-platform/tests/webusb/usbConnectionEvent.https.any.js
new file mode 100644
index 0000000000..12ede9e6c6
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usbConnectionEvent.https.any.js
@@ -0,0 +1,22 @@
+// META: script=/resources/test-only-api.js
+// META: script=/webusb/resources/fake-devices.js
+// META: script=/webusb/resources/usb-helpers.js
+
+'use strict';
+
+usb_test(() => getFakeDevice()
+ .then(({ device }) => {
+ let evt = new USBConnectionEvent('connect', { device: device });
+ assert_equals(evt.type, 'connect');
+ assert_equals(evt.device, device);
+ }),
+ 'Can construct a USBConnectionEvent with a device');
+
+test(t => {
+ assert_throws_js(TypeError,
+ () => new USBConnectionEvent('connect', { device: null }));
+ assert_throws_js(TypeError,
+ () => new USBConnectionEvent('connect', {}));
+}, 'Cannot construct a USBConnectionEvent without a device');
+
+done();
diff --git a/testing/web-platform/tests/webusb/usbDevice-iframe.https.html b/testing/web-platform/tests/webusb/usbDevice-iframe.https.html
new file mode 100644
index 0000000000..9e90adedc7
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usbDevice-iframe.https.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/test-only-api.js"></script>
+<script src="resources/fake-devices.js"></script>
+<script src="resources/usb-helpers.js"></script>
+<script>
+'use strict';
+
+async function sleep(timeout) {
+ return new Promise(resolve => {
+ step_timeout(() => {
+ resolve();
+ }, timeout);
+ });
+}
+
+async function connectInIframe() {
+ let iframe = document.createElement('iframe');
+ let opened = false;
+
+ iframe.src = 'resources/open-in-iframe.html';
+ document.body.appendChild(iframe);
+
+ await navigator.usb.test.attachToContext(iframe);
+ function nextIFrameMessage() {
+ return new Promise(resolve => window.addEventListener(
+ 'message', e => resolve(e.data)));
+ }
+ iframe.contentWindow.postMessage('ConnectEvent', '*');
+
+ assert_equals('Ready', (await nextIFrameMessage()));
+ let fakeDevice = navigator.usb.test.addFakeDevice(fakeDeviceInit);
+ let closedPromise = new Promise(resolve => fakeDevice.onclose = resolve)
+ .then(() => assert_true(opened));
+
+ assert_equals('Success', (await nextIFrameMessage()));
+ opened = true;
+ return { iframe, closedPromise };
+}
+
+usb_test(async () => {
+ let { iframe, closedPromise } = await connectInIframe();
+ document.body.removeChild(iframe);
+ await closedPromise;
+}, 'detaching iframe disconnects device.');
+
+usb_test(async () => {
+ let { iframe, closedPromise } = await connectInIframe();
+ iframe.src = 'about:blank';
+ await closedPromise;
+}, 'navigating iframe disconnects device.');
+
+usb_test(async (t) => {
+ let iframe = document.createElement('iframe');
+ iframe.src = 'resources/open-in-iframe.html';
+ iframe.allow = 'usb \'none\'';
+
+ await Promise.all([
+ new Promise(resolve => {
+ document.body.appendChild(iframe);
+ iframe.addEventListener('load', resolve);
+ }),
+ // This will wait for ReadyForAttachment event from iframe loading.
+ navigator.usb.test.attachToContext(iframe),
+ ]);
+
+ let messageWatcher = new EventWatcher(t, window, 'message');
+ iframe.contentWindow.postMessage('ConnectEvent', '*');
+ let messageEvent = await messageWatcher.wait_for('message');
+ assert_equals(messageEvent.data, 'Ready');
+
+ // This isn't necessary as the expected scenario shouldn't send any mojo
+ // request. However, in order to capture a bug that doesn't reject adding
+ // event listener, time delay here is to allow mojo request to be intercepted
+ // after iframe adding connect event listener.
+ await sleep(100);
+
+ // If device connect event fires, EventWatcher will assert for an unexpected
+ // event.
+ navigator.usb.test.addFakeDevice(fakeDeviceInit);
+ // Time delay here is to allow event to be fired if any.
+ await sleep(100);
+}, 'Connect event is not fired in iframe with usb disallowed.');
+
+</script>
diff --git a/testing/web-platform/tests/webusb/usbDevice-same-objecct.https.any.js b/testing/web-platform/tests/webusb/usbDevice-same-objecct.https.any.js
new file mode 100644
index 0000000000..088b4a258a
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usbDevice-same-objecct.https.any.js
@@ -0,0 +1,26 @@
+// META: script=/resources/test-only-api.js
+// META: script=/webusb/resources/fake-devices.js
+// META: script=/webusb/resources/usb-helpers.js
+'use strict';
+
+usb_test(async () => {
+ const {device} = await getFakeDevice();
+ await device.open();
+
+ for (const configuration of device.configurations) {
+ await device.selectConfiguration(configuration.configurationValue);
+ assert_equals(device.configuration, configuration);
+
+ for (const interfaceObj of configuration.interfaces) {
+ await device.claimInterface(interfaceObj.interfaceNumber);
+
+ for (const alternate of interfaceObj.alternates) {
+ await device.selectAlternateInterface(
+ interfaceObj.interfaceNumber, alternate.alternateSetting);
+ assert_equals(interfaceObj.alternate, alternate);
+ }
+ await device.releaseInterface(interfaceObj.interfaceNumber);
+ }
+ }
+ await device.close();
+}, '[SameObject] test for instances within USBDevice.');
diff --git a/testing/web-platform/tests/webusb/usbDevice-worker.https.html b/testing/web-platform/tests/webusb/usbDevice-worker.https.html
new file mode 100644
index 0000000000..940120495b
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usbDevice-worker.https.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/test-only-api.js"></script>
+<script src="/webusb/resources/fake-devices.js"></script>
+<script src="/webusb/resources/usb-helpers.js"></script>
+<script>
+'use strict';
+
+async function connectInWorker() {
+ let worker = new Worker('/webusb/resources/open-in-worker.js');
+ let opened = false;
+
+ await navigator.usb.test.attachToContext(worker);
+ function nextWorkerMessage() {
+ return new Promise(resolve => worker.addEventListener(
+ 'message', e => resolve(e.data)));
+ }
+ worker.postMessage({ type: 'ConnectEvent' });
+
+ assert_equals('Ready', (await nextWorkerMessage()).type);
+ let fakeDevice = navigator.usb.test.addFakeDevice(fakeDeviceInit);
+ let closedPromise = new Promise(resolve => fakeDevice.onclose = resolve)
+ .then(() => assert_true(opened));
+
+ assert_equals('Success', (await nextWorkerMessage()).type);
+ opened = true;
+ return { worker, closedPromise };
+}
+
+usb_test(async () => {
+ let { worker, closedPromise } = await connectInWorker();
+ worker.terminate();
+ await closedPromise;
+}, 'terminating worker disconnects device.');
+</script>
diff --git a/testing/web-platform/tests/webusb/usbDevice.https.any.js b/testing/web-platform/tests/webusb/usbDevice.https.any.js
new file mode 100644
index 0000000000..b1b0c133ce
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usbDevice.https.any.js
@@ -0,0 +1,1249 @@
+// META: timeout=long
+// META: script=/resources/test-only-api.js
+// META: script=/webusb/resources/fake-devices.js
+// META: script=/webusb/resources/usb-helpers.js
+'use strict';
+
+function detachBuffer(buffer) {
+ if (self.GLOBAL.isWindow())
+ window.postMessage('', '*', [buffer]);
+ else
+ self.postMessage('', [buffer]);
+}
+
+usb_test((t) => {
+ return getFakeDevice().then(({device, fakeDevice}) => {
+ return waitForDisconnect(fakeDevice)
+ .then(() => promise_rejects_dom(t, 'NotFoundError', device.open()));
+ });
+}, 'open rejects when called on a disconnected device');
+
+usb_test(() => {
+ return getFakeDevice().then(({ device, fakeDevice }) => {
+ return device.open()
+ .then(() => waitForDisconnect(fakeDevice))
+ .then(() => {
+ assert_false(device.opened);
+ });
+ });
+}, 'disconnection closes the device');
+
+usb_test(() => {
+ return getFakeDevice().then(({ device }) => {
+ assert_false(device.opened);
+ return device.open().then(() => {
+ assert_true(device.opened);
+ return device.close().then(() => {
+ assert_false(device.opened);
+ });
+ });
+ });
+}, 'a device can be opened and closed');
+
+usb_test(() => {
+ return getFakeDevice().then(({ device }) => {
+ return device.open()
+ .then(() => device.open())
+ .then(() => device.open())
+ .then(() => device.open())
+ .then(() => device.close())
+ .then(() => device.close())
+ .then(() => device.close())
+ .then(() => device.close());
+ });
+}, 'open and close can be called multiple times');
+
+usb_test(async (t) => {
+ let { device } = await getFakeDevice();
+ await Promise.all([
+ device.open(),
+ promise_rejects_dom(t, 'InvalidStateError', device.open()),
+ promise_rejects_dom(t, 'InvalidStateError', device.close()),
+ ]);
+ await Promise.all([
+ device.close(),
+ promise_rejects_dom(t, 'InvalidStateError', device.open()),
+ promise_rejects_dom(t, 'InvalidStateError', device.close()),
+ ]);
+}, 'open and close cannot be called again while open or close are in progress');
+
+usb_test(async (t) => {
+ let { device } = await getFakeDevice();
+ await device.open();
+ return Promise.all([
+ device.selectConfiguration(1),
+ promise_rejects_dom(t, 'InvalidStateError', device.claimInterface(0)),
+ promise_rejects_dom(t, 'InvalidStateError', device.releaseInterface(0)),
+ promise_rejects_dom(t, 'InvalidStateError', device.open()),
+ promise_rejects_dom(t, 'InvalidStateError', device.selectConfiguration(1)),
+ promise_rejects_dom(t, 'InvalidStateError', device.reset()),
+ promise_rejects_dom(
+ t, 'InvalidStateError', device.selectAlternateInterface(0, 0)),
+ promise_rejects_dom(t, 'InvalidStateError', device.controlTransferOut({
+ requestType: 'standard',
+ recipient: 'interface',
+ request: 0x42,
+ value: 0x1234,
+ index: 0x0000,
+ })),
+ promise_rejects_dom(
+ t, 'InvalidStateError',
+ device.controlTransferOut(
+ {
+ requestType: 'standard',
+ recipient: 'interface',
+ request: 0x42,
+ value: 0x1234,
+ index: 0x0000,
+ },
+ new Uint8Array([1, 2, 3]))),
+ promise_rejects_dom(
+ t, 'InvalidStateError',
+ device.controlTransferIn(
+ {
+ requestType: 'standard',
+ recipient: 'interface',
+ request: 0x42,
+ value: 0x1234,
+ index: 0x0000
+ },
+ 0)),
+ promise_rejects_dom(t, 'InvalidStateError', device.close()),
+ ]);
+}, 'device operations reject if an device state change is in progress');
+
+usb_test((t) => {
+ return getFakeDevice().then(({device, fakeDevice}) => {
+ return device.open()
+ .then(() => waitForDisconnect(fakeDevice))
+ .then(() => promise_rejects_dom(t, 'NotFoundError', device.close()));
+ });
+}, 'close rejects when called on a disconnected device');
+
+usb_test((t) => {
+ return getFakeDevice().then(({device, fakeDevice}) => {
+ return device.open()
+ .then(() => waitForDisconnect(fakeDevice))
+ .then(
+ () => promise_rejects_dom(
+ t, 'NotFoundError', device.selectConfiguration(1)));
+ });
+}, 'selectConfiguration rejects when called on a disconnected device');
+
+usb_test((t) => {
+ return getFakeDevice().then(({device}) => Promise.all([
+ promise_rejects_dom(t, 'InvalidStateError', device.selectConfiguration(1)),
+ promise_rejects_dom(t, 'InvalidStateError', device.claimInterface(0)),
+ promise_rejects_dom(t, 'InvalidStateError', device.releaseInterface(0)),
+ promise_rejects_dom(
+ t, 'InvalidStateError', device.selectAlternateInterface(0, 1)),
+ promise_rejects_dom(
+ t, 'InvalidStateError',
+ device.controlTransferIn(
+ {
+ requestType: 'vendor',
+ recipient: 'device',
+ request: 0x42,
+ value: 0x1234,
+ index: 0x5678
+ },
+ 7)),
+ promise_rejects_dom(
+ t, 'InvalidStateError',
+ device.controlTransferOut(
+ {
+ requestType: 'vendor',
+ recipient: 'device',
+ request: 0x42,
+ value: 0x1234,
+ index: 0x5678
+ },
+ new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]))),
+ promise_rejects_dom(t, 'InvalidStateError', device.clearHalt('in', 1)),
+ promise_rejects_dom(t, 'InvalidStateError', device.transferIn(1, 8)),
+ promise_rejects_dom(
+ t, 'InvalidStateError', device.transferOut(1, new ArrayBuffer(8))),
+ promise_rejects_dom(
+ t, 'InvalidStateError', device.isochronousTransferIn(1, [8])),
+ promise_rejects_dom(
+ t, 'InvalidStateError',
+ device.isochronousTransferOut(1, new ArrayBuffer(8), [8])),
+ promise_rejects_dom(t, 'InvalidStateError', device.reset())
+ ]));
+}, 'methods requiring it reject when the device is not open');
+
+usb_test(() => {
+ return getFakeDevice().then(({ device }) => {
+ assert_equals(device.configuration, null);
+ return device.open()
+ .then(() => {
+ assert_equals(device.configuration, null);
+ return device.selectConfiguration(1);
+ })
+ .then(() => {
+ assertDeviceInfoEquals(
+ device.configuration, fakeDeviceInit.configurations[0]);
+ })
+ .then(() => device.close());
+ });
+}, 'device configuration can be set and queried');
+
+usb_test(async () => {
+ let { device } = await getFakeDevice();
+ assert_equals(device.configuration, null);
+ await device.open();
+ assert_equals(device.configuration, null);
+ await device.selectConfiguration(1);
+ await device.selectConfiguration(1);
+ assertDeviceInfoEquals(
+ device.configuration, fakeDeviceInit.configurations[0]);
+ await device.selectConfiguration(2);
+ assertDeviceInfoEquals(
+ device.configuration, fakeDeviceInit.configurations[1]);
+ await device.close();
+}, 'a device configuration value can be set again');
+
+usb_test((t) => {
+ return getFakeDevice().then(({ device }) => {
+ assert_equals(device.configuration, null);
+ return device.open()
+ .then(
+ () => promise_rejects_dom(
+ t, 'NotFoundError', device.selectConfiguration(10)))
+ .then(() => device.close());
+ });
+}, 'selectConfiguration rejects on invalid configurations');
+
+usb_test((t) => {
+ return getFakeDevice().then(({ device }) => {
+ assert_equals(device.configuration, null);
+ return device.open()
+ .then(() => Promise.all([
+ promise_rejects_dom(t, 'InvalidStateError', device.claimInterface(0)),
+ promise_rejects_dom(
+ t, 'InvalidStateError', device.releaseInterface(0)),
+ promise_rejects_dom(
+ t, 'InvalidStateError', device.selectAlternateInterface(0, 1)),
+ promise_rejects_dom(
+ t, 'InvalidStateError', device.clearHalt('in', 1)),
+ promise_rejects_dom(t, 'InvalidStateError', device.transferIn(1, 8)),
+ promise_rejects_dom(
+ t, 'InvalidStateError',
+ device.transferOut(1, new ArrayBuffer(8))),
+ promise_rejects_dom(
+ t, 'InvalidStateError', device.isochronousTransferIn(1, [8])),
+ promise_rejects_dom(
+ t, 'InvalidStateError',
+ device.isochronousTransferOut(1, new ArrayBuffer(8), [8])),
+ ]))
+ .then(() => device.close());
+ });
+}, 'methods requiring it reject when the device is unconfigured');
+
+usb_test(async () => {
+ let { device } = await getFakeDevice();
+ await device.open();
+ await device.selectConfiguration(1);
+ assert_false(device.configuration.interfaces[0].claimed);
+ assert_false(device.configuration.interfaces[1].claimed);
+
+ await device.claimInterface(0);
+ assert_true(device.configuration.interfaces[0].claimed);
+ assert_false(device.configuration.interfaces[1].claimed);
+
+ await device.claimInterface(1);
+ assert_true(device.configuration.interfaces[0].claimed);
+ assert_true(device.configuration.interfaces[1].claimed);
+
+ await device.releaseInterface(0);
+ assert_false(device.configuration.interfaces[0].claimed);
+ assert_true(device.configuration.interfaces[1].claimed);
+
+ await device.releaseInterface(1);
+ assert_false(device.configuration.interfaces[0].claimed);
+ assert_false(device.configuration.interfaces[1].claimed);
+
+ await device.close();
+}, 'interfaces can be claimed and released');
+
+usb_test(async () => {
+ let { device } = await getFakeDevice();
+ await device.open();
+ await device.selectConfiguration(1);
+ assert_false(device.configuration.interfaces[0].claimed);
+ assert_false(device.configuration.interfaces[1].claimed);
+
+ await Promise.all([device.claimInterface(0),
+ device.claimInterface(1)]);
+ assert_true(device.configuration.interfaces[0].claimed);
+ assert_true(device.configuration.interfaces[1].claimed);
+
+ await Promise.all([device.releaseInterface(0),
+ device.releaseInterface(1)]);
+ assert_false(device.configuration.interfaces[0].claimed);
+ assert_false(device.configuration.interfaces[1].claimed);
+
+ await device.close();
+}, 'interfaces can be claimed and released in parallel');
+
+usb_test(async () => {
+ let { device } = await getFakeDevice()
+ await device.open();
+ await device.selectConfiguration(1);
+ await device.claimInterface(0);
+ assert_true(device.configuration.interfaces[0].claimed);
+ await device.claimInterface(0);
+ assert_true(device.configuration.interfaces[0].claimed);
+ await device.close();
+}, 'an interface can be claimed multiple times');
+
+usb_test(async () => {
+ let { device } = await getFakeDevice();
+ await device.open();
+ await device.selectConfiguration(1);
+ await device.claimInterface(0);
+ assert_true(device.configuration.interfaces[0].claimed);
+ await device.releaseInterface(0);
+ assert_false(device.configuration.interfaces[0].claimed);
+ await device.releaseInterface(0);
+ assert_false(device.configuration.interfaces[0].claimed);
+ await device.close();
+}, 'an interface can be released multiple times');
+
+usb_test(async (t) => {
+ let { device } = await getFakeDevice();
+ await device.open();
+ await device.selectConfiguration(1);
+ return Promise.all([
+ device.claimInterface(0),
+ promise_rejects_dom(t, 'InvalidStateError', device.claimInterface(0)),
+ promise_rejects_dom(t, 'InvalidStateError', device.releaseInterface(0)),
+ promise_rejects_dom(t, 'InvalidStateError', device.open()),
+ promise_rejects_dom(t, 'InvalidStateError', device.selectConfiguration(1)),
+ promise_rejects_dom(t, 'InvalidStateError', device.reset()),
+ promise_rejects_dom(
+ t, 'InvalidStateError', device.selectAlternateInterface(0, 0)),
+ promise_rejects_dom(t, 'InvalidStateError', device.controlTransferOut({
+ requestType: 'standard',
+ recipient: 'interface',
+ request: 0x42,
+ value: 0x1234,
+ index: 0x0000,
+ })),
+ promise_rejects_dom(
+ t, 'InvalidStateError',
+ device.controlTransferOut(
+ {
+ requestType: 'standard',
+ recipient: 'interface',
+ request: 0x42,
+ value: 0x1234,
+ index: 0x0000,
+ },
+ new Uint8Array([1, 2, 3]))),
+ promise_rejects_dom(
+ t, 'InvalidStateError',
+ device.controlTransferIn(
+ {
+ requestType: 'standard',
+ recipient: 'interface',
+ request: 0x42,
+ value: 0x1234,
+ index: 0x0000
+ },
+ 0)),
+ promise_rejects_dom(t, 'InvalidStateError', device.close()),
+ ]);
+}, 'device operations reject if an interface state change is in progress');
+
+usb_test(async () => {
+ let { device } = await getFakeDevice();
+ await device.open();
+ await device.selectConfiguration(1);
+ await device.claimInterface(0);
+ assert_true(device.configuration.interfaces[0].claimed);
+ await device.close(0);
+ assert_false(device.configuration.interfaces[0].claimed);
+}, 'interfaces are released on close');
+
+usb_test((t) => {
+ return getFakeDevice().then(({device}) => {
+ return device.open()
+ .then(() => device.selectConfiguration(1))
+ .then(() => Promise.all([
+ promise_rejects_dom(t, 'NotFoundError', device.claimInterface(2)),
+ promise_rejects_dom(t, 'NotFoundError', device.releaseInterface(2)),
+ ]))
+ .then(() => device.close());
+ });
+}, 'a non-existent interface cannot be claimed or released');
+
+usb_test((t) => {
+ return getFakeDevice().then(({device, fakeDevice}) => {
+ return device.open()
+ .then(() => device.selectConfiguration(1))
+ .then(() => waitForDisconnect(fakeDevice))
+ .then(
+ () => promise_rejects_dom(
+ t, 'NotFoundError', device.claimInterface(0)));
+ });
+}, 'claimInterface rejects when called on a disconnected device');
+
+usb_test((t) => {
+ return getFakeDevice().then(({device, fakeDevice}) => {
+ return device.open()
+ .then(() => device.selectConfiguration(1))
+ .then(() => device.claimInterface(0))
+ .then(() => waitForDisconnect(fakeDevice))
+ .then(
+ () => promise_rejects_dom(
+ t, 'NotFoundError', device.releaseInterface(0)));
+ });
+}, 'releaseInterface rejects when called on a disconnected device');
+
+usb_test(() => {
+ return getFakeDevice().then(({ device }) => {
+ return device.open()
+ .then(() => device.selectConfiguration(2))
+ .then(() => device.claimInterface(0))
+ .then(() => device.selectAlternateInterface(0, 1))
+ .then(() => device.close());
+ });
+}, 'can select an alternate interface');
+
+usb_test(
+ async () => {
+ const {device} = await getFakeDevice();
+ await device.open();
+ await device.selectConfiguration(3);
+ await device.claimInterface(2);
+ await device.selectAlternateInterface(2, 0);
+ await device.close();
+ },
+ 'can select an alternate interface on a setting with non-sequential ' +
+ 'interface number');
+
+usb_test(
+ async () => {
+ const {device} = await getFakeDevice();
+ await device.open();
+ await device.selectConfiguration(3);
+ await device.claimInterface(0);
+ await device.selectAlternateInterface(0, 2);
+ await device.close();
+ },
+ 'can select an alternate interface on a setting with non-sequential ' +
+ 'alternative setting value');
+
+usb_test((t) => {
+ return getFakeDevice().then(({device}) => {
+ return device.open()
+ .then(() => device.selectConfiguration(2))
+ .then(() => device.claimInterface(0))
+ .then(
+ () => promise_rejects_dom(
+ t, 'NotFoundError', device.selectAlternateInterface(0, 2)))
+ .then(() => device.close());
+ });
+}, 'cannot select a non-existent alternate interface');
+
+usb_test((t) => {
+ return getFakeDevice().then(({device, fakeDevice}) => {
+ return device.open()
+ .then(() => device.selectConfiguration(2))
+ .then(() => device.claimInterface(0))
+ .then(() => waitForDisconnect(fakeDevice))
+ .then(
+ () => promise_rejects_dom(
+ t, 'NotFoundError', device.selectAlternateInterface(0, 1)));
+ });
+}, 'selectAlternateInterface rejects when called on a disconnected device');
+
+usb_test(async () => {
+ let { device } = await getFakeDevice();
+ let usbRequestTypes = ['standard', 'class', 'vendor'];
+ let usbRecipients = ['device', 'interface', 'endpoint', 'other'];
+ await device.open();
+ await device.selectConfiguration(1);
+ await device.claimInterface(0);
+ await device.selectAlternateInterface(0, 0);
+ for (const requestType of usbRequestTypes) {
+ for (const recipient of usbRecipients) {
+ let index = recipient === 'interface' ? 0x5600 : 0x5681;
+ let result = await device.controlTransferIn({
+ requestType: requestType,
+ recipient: recipient,
+ request: 0x42,
+ value: 0x1234,
+ index: index
+ }, 7);
+ assert_true(result instanceof USBInTransferResult);
+ assert_equals(result.status, 'ok');
+ assert_equals(result.data.byteLength, 7);
+ assert_equals(result.data.getUint16(0), 0x07);
+ assert_equals(result.data.getUint8(2), 0x42);
+ assert_equals(result.data.getUint16(3), 0x1234);
+ assert_equals(result.data.getUint16(5), index);
+ }
+ }
+ await device.close();
+}, 'can issue all types of IN control transfers');
+
+usb_test(async () => {
+ let { device } = await getFakeDevice();
+ let usbRequestTypes = ['standard', 'class', 'vendor'];
+ let usbRecipients = ['device', 'other'];
+ await device.open();
+ await Promise.all(usbRequestTypes.flatMap(requestType => {
+ return usbRecipients.map(async recipient => {
+ let result = await device.controlTransferIn({
+ requestType: requestType,
+ recipient: recipient,
+ request: 0x42,
+ value: 0x1234,
+ index: 0x5678
+ }, 7);
+ assert_true(result instanceof USBInTransferResult);
+ assert_equals(result.status, 'ok');
+ assert_equals(result.data.byteLength, 7);
+ assert_equals(result.data.getUint16(0), 0x07);
+ assert_equals(result.data.getUint8(2), 0x42);
+ assert_equals(result.data.getUint16(3), 0x1234);
+ assert_equals(result.data.getUint16(5), 0x5678);
+ });
+ }));
+ await device.close();
+}, 'device-scope IN control transfers don\'t require configuration');
+
+usb_test(async (t) => {
+ let { device } = await getFakeDevice();
+ let usbRequestTypes = ['standard', 'class', 'vendor'];
+ let usbRecipients = ['interface', 'endpoint'];
+ await device.open();
+ await Promise.all(usbRequestTypes.flatMap(requestType => {
+ return usbRecipients.map(recipient => {
+ let index = recipient === 'interface' ? 0x5600 : 0x5681;
+ return promise_rejects_dom(
+ t, 'InvalidStateError',
+ device.controlTransferIn(
+ {
+ requestType: requestType,
+ recipient: recipient,
+ request: 0x42,
+ value: 0x1234,
+ index: index
+ },
+ 7));
+ });
+ }));
+ await device.close();
+}, 'interface-scope IN control transfers require configuration');
+
+usb_test(async (t) => {
+ let { device } = await getFakeDevice();
+ let usbRequestTypes = ['standard', 'class', 'vendor'];
+ let usbRecipients = ['interface', 'endpoint'];
+ await device.open();
+ await device.selectConfiguration(1);
+ await Promise.all(usbRequestTypes.flatMap(requestType => {
+ return [
+ promise_rejects_dom(
+ t, 'InvalidStateError',
+ device.controlTransferIn(
+ {
+ requestType: requestType,
+ recipient: 'interface',
+ request: 0x42,
+ value: 0x1234,
+ index: 0x5600
+ },
+ 7)),
+ promise_rejects_dom(
+ t, 'NotFoundError',
+ device.controlTransferIn(
+ {
+ requestType: requestType,
+ recipient: 'endpoint',
+ request: 0x42,
+ value: 0x1234,
+ index: 0x5681
+ },
+ 7))
+ ];
+ }));
+ await device.close();
+}, 'interface-scope IN control transfers require claiming the interface');
+
+usb_test((t) => {
+ return getFakeDevice().then(({device, fakeDevice}) => {
+ return device.open()
+ .then(() => device.selectConfiguration(1))
+ .then(() => waitForDisconnect(fakeDevice))
+ .then(
+ () => promise_rejects_dom(
+ t, 'NotFoundError',
+ device.controlTransferIn(
+ {
+ requestType: 'vendor',
+ recipient: 'device',
+ request: 0x42,
+ value: 0x1234,
+ index: 0x5678
+ },
+ 7)));
+ });
+}, 'controlTransferIn rejects when called on a disconnected device');
+
+usb_test(async () => {
+ let { device } = await getFakeDevice();
+ let usbRequestTypes = ['standard', 'class', 'vendor'];
+ let usbRecipients = ['device', 'interface', 'endpoint', 'other'];
+ let dataArray = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
+ let dataTypes = [dataArray, dataArray.buffer];
+ await device.open();
+ await device.selectConfiguration(1);
+ await device.claimInterface(0);
+ await device.selectAlternateInterface(0, 0);
+ for (const requestType of usbRequestTypes) {
+ for (const recipient of usbRecipients) {
+ let index = recipient === 'interface' ? 0x5600 : 0x5681;
+ let transferParams = {
+ requestType: requestType,
+ recipient: recipient,
+ request: 0x42,
+ value: 0x1234,
+ index: index
+ };
+ for (const data of dataTypes) {
+ let result = await device.controlTransferOut(transferParams, data);
+ assert_true(result instanceof USBOutTransferResult);
+ assert_equals(result.status, 'ok');
+ assert_equals(result.bytesWritten, 8);
+ }
+ let result = await device.controlTransferOut(transferParams);
+ assert_true(result instanceof USBOutTransferResult);
+ assert_equals(result.status, 'ok');
+ }
+ }
+ await device.close();
+}, 'can issue all types of OUT control transfers');
+
+usb_test(async () => {
+ let { device } = await getFakeDevice();
+ let usbRequestTypes = ['standard', 'class', 'vendor'];
+ let usbRecipients = ['device', 'other'];
+ let dataArray = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
+ let dataTypes = [dataArray, dataArray.buffer];
+ await device.open();
+ await Promise.all(usbRequestTypes.flatMap(requestType => {
+ return usbRecipients.flatMap(recipient => {
+ let transferParams = {
+ requestType: requestType,
+ recipient: recipient,
+ request: 0x42,
+ value: 0x1234,
+ index: 0x5678
+ };
+ return dataTypes.map(async data => {
+ let result = await device.controlTransferOut(transferParams, data);
+ assert_true(result instanceof USBOutTransferResult);
+ assert_equals(result.status, 'ok');
+ assert_equals(result.bytesWritten, 8);
+ }).push((async () => {
+ let result = await device.controlTransferOut(transferParams);
+ assert_true(result instanceof USBOutTransferResult);
+ assert_equals(result.status, 'ok');
+ })());
+ });
+ }));
+ await device.close();
+}, 'device-scope OUT control transfers don\'t require configuration');
+
+usb_test(async (t) => {
+ let { device } = await getFakeDevice();
+ let usbRequestTypes = ['standard', 'class', 'vendor'];
+ let usbRecipients = ['interface', 'endpoint'];
+ let dataArray = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
+ let dataTypes = [dataArray, dataArray.buffer];
+ await device.open();
+ await Promise.all(usbRequestTypes.flatMap(requestType => {
+ return usbRecipients.flatMap(recipient => {
+ let index = recipient === 'interface' ? 0x5600 : 0x5681;
+ let transferParams = {
+ requestType: requestType,
+ recipient: recipient,
+ request: 0x42,
+ value: 0x1234,
+ index: index
+ };
+ return dataTypes
+ .map(data => {
+ return promise_rejects_dom(
+ t, 'InvalidStateError',
+ device.controlTransferOut(transferParams, data));
+ })
+ .push(promise_rejects_dom(
+ t, 'InvalidStateError',
+ device.controlTransferOut(transferParams)));
+ });
+ }));
+ await device.close();
+}, 'interface-scope OUT control transfers require configuration');
+
+usb_test(async (t) => {
+ let { device } = await getFakeDevice();
+ let usbRequestTypes = ['standard', 'class', 'vendor'];
+ let usbRecipients = ['interface', 'endpoint'];
+ let dataArray = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
+ let dataTypes = [dataArray, dataArray.buffer];
+ await device.open();
+ await device.selectConfiguration(1);
+ await Promise.all(usbRequestTypes.flatMap(requestType => {
+ return usbRecipients.flatMap(recipient => {
+ let index = recipient === 'interface' ? 0x5600 : 0x5681;
+ let error =
+ recipient === 'interface' ? 'InvalidStateError' : 'NotFoundError';
+ let transferParams = {
+ requestType: requestType,
+ recipient: recipient,
+ request: 0x42,
+ value: 0x1234,
+ index: index
+ };
+ return dataTypes
+ .map(data => {
+ return promise_rejects_dom(
+ t, error, device.controlTransferOut(transferParams, data));
+ })
+ .push(promise_rejects_dom(
+ t, error, device.controlTransferOut(transferParams)));
+ });
+ }));
+ await device.close();
+}, 'interface-scope OUT control transfers an interface claim');
+
+usb_test((t) => {
+ return getFakeDevice().then(({device, fakeDevice}) => {
+ return device.open()
+ .then(() => device.selectConfiguration(1))
+ .then(() => waitForDisconnect(fakeDevice))
+ .then(
+ () => promise_rejects_dom(
+ t, 'NotFoundError',
+ device.controlTransferOut(
+ {
+ requestType: 'vendor',
+ recipient: 'device',
+ request: 0x42,
+ value: 0x1234,
+ index: 0x5678
+ },
+ new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]))));
+ });
+}, 'controlTransferOut rejects when called on a disconnected device');
+
+usb_test(async (t) => {
+ let { device } = await getFakeDevice();
+ await device.open();
+ await device.selectConfiguration(1);
+ await device.claimInterface(0);
+ await Promise.all([
+ promise_rejects_js(
+ t, TypeError,
+ device.controlTransferOut(
+ {
+ requestType: 'invalid',
+ recipient: 'device',
+ request: 0x42,
+ value: 0x1234,
+ index: 0x5678
+ },
+ new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]))),
+ promise_rejects_js(
+ t, TypeError,
+ device.controlTransferIn(
+ {
+ requestType: 'invalid',
+ recipient: 'device',
+ request: 0x42,
+ value: 0x1234,
+ index: 0x5678
+ },
+ 0)),
+ ]);
+ await device.close();
+}, 'control transfers with a invalid request type reject');
+
+usb_test(async (t) => {
+ let { device } = await getFakeDevice();
+ await device.open();
+ await device.selectConfiguration(1);
+ await device.claimInterface(0);
+ await Promise.all([
+ promise_rejects_js(
+ t, TypeError,
+ device.controlTransferOut(
+ {
+ requestType: 'vendor',
+ recipient: 'invalid',
+ request: 0x42,
+ value: 0x1234,
+ index: 0x5678
+ },
+ new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]))),
+ promise_rejects_js(
+ t, TypeError,
+ device.controlTransferIn(
+ {
+ requestType: 'vendor',
+ recipient: 'invalid',
+ request: 0x42,
+ value: 0x1234,
+ index: 0x5678
+ },
+ 0)),
+ ]);
+}, 'control transfers with a invalid recipient type reject');
+
+usb_test(async (t) => {
+ let { device } = await getFakeDevice();
+ await device.open();
+ await device.selectConfiguration(1);
+ await device.claimInterface(0);
+ await Promise.all([
+ promise_rejects_dom(
+ t, 'NotFoundError',
+ device.controlTransferOut(
+ {
+ requestType: 'vendor',
+ recipient: 'interface',
+ request: 0x42,
+ value: 0x1234,
+ index: 0x0002 // Last byte of index is interface number.
+ },
+ new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]))),
+ promise_rejects_dom(
+ t, 'NotFoundError',
+ device.controlTransferIn(
+ {
+ requestType: 'vendor',
+ recipient: 'interface',
+ request: 0x42,
+ value: 0x1234,
+ index: 0x0002 // Last byte of index is interface number.
+ },
+ 0)),
+ ]);
+}, 'control transfers to a non-existant interface reject');
+
+usb_test((t) => {
+ return getFakeDevice().then(({ device }) => {
+ let interfaceRequest = {
+ requestType: 'vendor',
+ recipient: 'interface',
+ request: 0x42,
+ value: 0x1234,
+ index: 0x5600 // Last byte of index is interface number.
+ };
+ let endpointRequest = {
+ requestType: 'vendor',
+ recipient: 'endpoint',
+ request: 0x42,
+ value: 0x1234,
+ index: 0x5681 // Last byte of index is endpoint address.
+ };
+ let data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
+ return device.open()
+ .then(() => device.selectConfiguration(1))
+ .then(() => Promise.all([
+ promise_rejects_dom(
+ t, 'InvalidStateError',
+ device.controlTransferIn(interfaceRequest, 7)),
+ promise_rejects_dom(
+ t, 'NotFoundError', device.controlTransferIn(endpointRequest, 7)),
+ promise_rejects_dom(
+ t, 'InvalidStateError',
+ device.controlTransferOut(interfaceRequest, data)),
+ promise_rejects_dom(
+ t, 'NotFoundError',
+ device.controlTransferOut(endpointRequest, data)),
+ ]))
+ .then(() => device.claimInterface(0))
+ .then(() => Promise.all([
+ device.controlTransferIn(interfaceRequest, 7).then(result => {
+ assert_true(result instanceof USBInTransferResult);
+ assert_equals(result.status, 'ok');
+ assert_equals(result.data.byteLength, 7);
+ assert_equals(result.data.getUint16(0), 0x07);
+ assert_equals(result.data.getUint8(2), 0x42);
+ assert_equals(result.data.getUint16(3), 0x1234);
+ assert_equals(result.data.getUint16(5), 0x5600);
+ }),
+ device.controlTransferIn(endpointRequest, 7).then(result => {
+ assert_true(result instanceof USBInTransferResult);
+ assert_equals(result.status, 'ok');
+ assert_equals(result.data.byteLength, 7);
+ assert_equals(result.data.getUint16(0), 0x07);
+ assert_equals(result.data.getUint8(2), 0x42);
+ assert_equals(result.data.getUint16(3), 0x1234);
+ assert_equals(result.data.getUint16(5), 0x5681);
+ }),
+ device.controlTransferOut(interfaceRequest, data),
+ device.controlTransferOut(endpointRequest, data),
+ ]))
+ .then(() => device.close());
+ });
+}, 'requests to interfaces and endpoint require an interface claim');
+
+usb_test(async () => {
+ const { device } = await getFakeDevice();
+ await device.open();
+ await device.selectConfiguration(1);
+ await device.claimInterface(0);
+
+ const transfer_params = {
+ requestType: 'vendor',
+ recipient: 'device',
+ request: 0,
+ value: 0,
+ index: 0
+ };
+
+ try {
+ const array_buffer = new ArrayBuffer(64 * 8);
+ const result =
+ await device.controlTransferOut(transfer_params, array_buffer);
+ assert_equals(result.status, 'ok');
+
+ detachBuffer(array_buffer);
+ await device.controlTransferOut(transfer_params, array_buffer);
+ assert_unreached();
+ } catch (e) {
+ assert_equals(e.code, DOMException.INVALID_STATE_ERR);
+ }
+
+ try {
+ const typed_array = new Uint8Array(64 * 8);
+ const result =
+ await device.controlTransferOut(transfer_params, typed_array);
+ assert_equals(result.status, 'ok');
+
+ detachBuffer(typed_array.buffer);
+ await device.controlTransferOut(transfer_params, typed_array);
+ assert_unreached();
+ } catch (e) {
+ assert_equals(e.code, DOMException.INVALID_STATE_ERR);
+ }
+}, 'controlTransferOut rejects if called with a detached buffer');
+
+usb_test(() => {
+ return getFakeDevice().then(({ device }) => {
+ return device.open()
+ .then(() => device.selectConfiguration(1))
+ .then(() => device.claimInterface(0))
+ .then(() => device.clearHalt('in', 1))
+ .then(() => device.close());
+ });
+}, 'can clear a halt condition');
+
+usb_test((t) => {
+ return getFakeDevice(t).then(({device, fakeDevice}) => {
+ return device.open()
+ .then(() => device.selectConfiguration(1))
+ .then(() => device.claimInterface(0))
+ .then(() => waitForDisconnect(fakeDevice))
+ .then(
+ () => promise_rejects_dom(
+ t, 'NotFoundError', device.clearHalt('in', 1)));
+ });
+}, 'clearHalt rejects when called on a disconnected device');
+
+usb_test((t) => {
+ return getFakeDevice().then(({ device }) => {
+ let data = new DataView(new ArrayBuffer(1024));
+ for (let i = 0; i < 1024; ++i)
+ data.setUint8(i, i & 0xff);
+ return device.open()
+ .then(() => device.selectConfiguration(1))
+ .then(() => device.claimInterface(0))
+ .then(() => Promise.all([
+ promise_rejects_dom(
+ t, 'NotFoundError', device.transferIn(2, 8)), // Unclaimed
+ promise_rejects_dom(
+ t, 'NotFoundError', device.transferIn(3, 8)), // Non-existent
+ promise_rejects_dom(t, 'IndexSizeError', device.transferIn(16, 8)),
+ promise_rejects_dom(
+ t, 'NotFoundError', device.transferOut(2, data)), // Unclaimed
+ promise_rejects_dom(
+ t, 'NotFoundError', device.transferOut(3, data)), // Non-existent
+ promise_rejects_dom(
+ t, 'IndexSizeError', device.transferOut(16, data)),
+ ]));
+ });
+}, 'transfers to unavailable endpoints are rejected');
+
+usb_test(() => {
+ return getFakeDevice().then(({ device }) => {
+ return device.open()
+ .then(() => device.selectConfiguration(1))
+ .then(() => device.claimInterface(0))
+ .then(() => device.transferIn(1, 8))
+ .then(result => {
+ assert_true(result instanceof USBInTransferResult);
+ assert_equals(result.status, 'ok');
+ assert_equals(result.data.byteLength, 8);
+ for (let i = 0; i < 8; ++i)
+ assert_equals(result.data.getUint8(i), i, 'mismatch at byte ' + i);
+ return device.close();
+ });
+ });
+}, 'can issue IN interrupt transfer');
+
+usb_test(() => {
+ return getFakeDevice().then(({ device }) => {
+ return device.open()
+ .then(() => device.selectConfiguration(1))
+ .then(() => device.claimInterface(1))
+ .then(() => device.transferIn(2, 1024))
+ .then(result => {
+ assert_true(result instanceof USBInTransferResult);
+ assert_equals(result.status, 'ok');
+ assert_equals(result.data.byteLength, 1024);
+ for (let i = 0; i < 1024; ++i)
+ assert_equals(result.data.getUint8(i), i & 0xff,
+ 'mismatch at byte ' + i);
+ return device.close();
+ });
+ });
+}, 'can issue IN bulk transfer');
+
+usb_test((t) => {
+ return getFakeDevice().then(({device, fakeDevice}) => {
+ return device.open()
+ .then(() => device.selectConfiguration(1))
+ .then(() => device.claimInterface(1))
+ .then(() => waitForDisconnect(fakeDevice))
+ .then(
+ () => promise_rejects_dom(
+ t, 'NotFoundError', device.transferIn(2, 1024)));
+ });
+}, 'transferIn rejects if called on a disconnected device');
+
+usb_test(() => {
+ return getFakeDevice().then(({ device }) => {
+ return device.open()
+ .then(() => device.selectConfiguration(1))
+ .then(() => device.claimInterface(1))
+ .then(() => {
+ let data = new DataView(new ArrayBuffer(1024));
+ for (let i = 0; i < 1024; ++i)
+ data.setUint8(i, i & 0xff);
+ return device.transferOut(2, data);
+ })
+ .then(result => {
+ assert_true(result instanceof USBOutTransferResult);
+ assert_equals(result.status, 'ok');
+ assert_equals(result.bytesWritten, 1024);
+ return device.close();
+ });
+ });
+}, 'can issue OUT bulk transfer');
+
+usb_test((t) => {
+ return getFakeDevice().then(({ device, fakeDevice }) => {
+ return device.open()
+ .then(() => device.selectConfiguration(1))
+ .then(() => device.claimInterface(1))
+ .then(() => {
+ let data = new DataView(new ArrayBuffer(1024));
+ for (let i = 0; i < 1024; ++i)
+ data.setUint8(i, i & 0xff);
+ return waitForDisconnect(fakeDevice)
+ .then(
+ () => promise_rejects_dom(
+ t, 'NotFoundError', device.transferOut(2, data)));
+ });
+ });
+}, 'transferOut rejects if called on a disconnected device');
+
+usb_test(async () => {
+ const { device } = await getFakeDevice();
+ await device.open();
+ await device.selectConfiguration(1);
+ await device.claimInterface(1);
+
+
+ try {
+ const array_buffer = new ArrayBuffer(64 * 8);
+ const result = await device.transferOut(2, array_buffer);
+ assert_equals(result.status, 'ok');
+
+ detachBuffer(array_buffer);
+ await device.transferOut(2, array_buffer);
+ assert_unreached();
+ } catch (e) {
+ assert_equals(e.code, DOMException.INVALID_STATE_ERR);
+ }
+
+ try {
+ const typed_array = new Uint8Array(64 * 8);
+ const result = await device.transferOut(2, typed_array);
+ assert_equals(result.status, 'ok');
+
+ detachBuffer(typed_array.buffer);
+ await device.transferOut(2, typed_array);
+ assert_unreached();
+ } catch (e) {
+ assert_equals(e.code, DOMException.INVALID_STATE_ERR);
+ }
+}, 'transferOut rejects if called with a detached buffer');
+
+usb_test(() => {
+ return getFakeDevice().then(({ device }) => {
+ return device.open()
+ .then(() => device.selectConfiguration(2))
+ .then(() => device.claimInterface(0))
+ .then(() => device.selectAlternateInterface(0, 1))
+ .then(() => device.isochronousTransferIn(
+ 1, [64, 64, 64, 64, 64, 64, 64, 64]))
+ .then(result => {
+ assert_true(result instanceof USBIsochronousInTransferResult);
+ assert_equals(result.data.byteLength, 64 * 8, 'buffer size');
+ assert_equals(result.packets.length, 8, 'number of packets');
+ let byteOffset = 0;
+ for (let i = 0; i < result.packets.length; ++i) {
+ assert_true(
+ result.packets[i] instanceof USBIsochronousInTransferPacket);
+ assert_equals(result.packets[i].status, 'ok');
+ assert_equals(result.packets[i].data.byteLength, 64);
+ assert_equals(result.packets[i].data.buffer, result.data.buffer);
+ assert_equals(result.packets[i].data.byteOffset, byteOffset);
+ for (let j = 0; j < 64; ++j)
+ assert_equals(result.packets[i].data.getUint8(j), j & 0xff,
+ 'mismatch at byte ' + j + ' of packet ' + i);
+ byteOffset += result.packets[i].data.byteLength;
+ }
+ return device.close();
+ });
+ });
+}, 'can issue IN isochronous transfer');
+
+usb_test((t) => {
+ return getFakeDevice().then(({device, fakeDevice}) => {
+ return device.open()
+ .then(() => device.selectConfiguration(2))
+ .then(() => device.claimInterface(0))
+ .then(() => device.selectAlternateInterface(0, 1))
+ .then(() => waitForDisconnect(fakeDevice))
+ .then(
+ () => promise_rejects_dom(
+ t, 'NotFoundError',
+ device.isochronousTransferIn(
+ 1, [64, 64, 64, 64, 64, 64, 64, 64])));
+ });
+}, 'isochronousTransferIn rejects when called on a disconnected device');
+
+usb_test(() => {
+ return getFakeDevice().then(({ device }) => {
+ return device.open()
+ .then(() => device.selectConfiguration(2))
+ .then(() => device.claimInterface(0))
+ .then(() => device.selectAlternateInterface(0, 1))
+ .then(() => {
+ let data = new DataView(new ArrayBuffer(64 * 8));
+ for (let i = 0; i < 8; ++i) {
+ for (let j = 0; j < 64; ++j)
+ data.setUint8(i * j, j & 0xff);
+ }
+ return device.isochronousTransferOut(
+ 1, data, [64, 64, 64, 64, 64, 64, 64, 64]);
+ })
+ .then(result => {
+ assert_true(result instanceof USBIsochronousOutTransferResult);
+ assert_equals(result.packets.length, 8, 'number of packets');
+ let byteOffset = 0;
+ for (let i = 0; i < result.packets.length; ++i) {
+ assert_true(
+ result.packets[i] instanceof USBIsochronousOutTransferPacket);
+ assert_equals(result.packets[i].status, 'ok');
+ assert_equals(result.packets[i].bytesWritten, 64);
+ }
+ return device.close();
+ });
+ });
+}, 'can issue OUT isochronous transfer');
+
+usb_test((t) => {
+ return getFakeDevice().then(({ device, fakeDevice }) => {
+ return device.open()
+ .then(() => device.selectConfiguration(2))
+ .then(() => device.claimInterface(0))
+ .then(() => device.selectAlternateInterface(0, 1))
+ .then(() => {
+ let data = new DataView(new ArrayBuffer(64 * 8));
+ for (let i = 0; i < 8; ++i) {
+ for (let j = 0; j < 64; ++j)
+ data.setUint8(i * j, j & 0xff);
+ }
+ return waitForDisconnect(fakeDevice)
+ .then(
+ () => promise_rejects_dom(
+ t, 'NotFoundError',
+ device.isochronousTransferOut(
+ 1, data, [64, 64, 64, 64, 64, 64, 64, 64])));
+ });
+ });
+}, 'isochronousTransferOut rejects when called on a disconnected device');
+
+usb_test(async () => {
+ const { device } = await getFakeDevice();
+ await device.open();
+ await device.selectConfiguration(2);
+ await device.claimInterface(0);
+ await device.selectAlternateInterface(0, 1);
+
+
+ try {
+ const array_buffer = new ArrayBuffer(64 * 8);
+ const result = await device.isochronousTransferOut(
+ 1, array_buffer, [64, 64, 64, 64, 64, 64, 64, 64]);
+ for (let i = 0; i < result.packets.length; ++i)
+ assert_equals(result.packets[i].status, 'ok');
+
+ detachBuffer(array_buffer);
+ await device.isochronousTransferOut(
+ 1, array_buffer, [64, 64, 64, 64, 64, 64, 64, 64]);
+ assert_unreached();
+ } catch (e) {
+ assert_equals(e.code, DOMException.INVALID_STATE_ERR);
+ }
+
+ try {
+ const typed_array = new Uint8Array(64 * 8);
+ const result = await device.isochronousTransferOut(
+ 1, typed_array, [64, 64, 64, 64, 64, 64, 64, 64]);
+ for (let i = 0; i < result.packets.length; ++i)
+ assert_equals(result.packets[i].status, 'ok');
+
+ detachBuffer(typed_array.buffer);
+ await device.isochronousTransferOut(
+ 1, typed_array, [64, 64, 64, 64, 64, 64, 64, 64]);
+ assert_unreached();
+ } catch (e) {
+ assert_equals(e.code, DOMException.INVALID_STATE_ERR);
+ }
+}, 'isochronousTransferOut rejects when called with a detached buffer');
+
+usb_test(() => {
+ return getFakeDevice().then(({ device }) => {
+ return device.open().then(() => device.reset()).then(() => device.close());
+ });
+}, 'can reset the device');
+
+usb_test((t) => {
+ return getFakeDevice().then(({device, fakeDevice}) => {
+ return device.open()
+ .then(() => waitForDisconnect(fakeDevice))
+ .then(() => promise_rejects_dom(t, 'NotFoundError', device.reset()));
+ });
+}, 'resetDevice rejects when called on a disconnected device');
diff --git a/testing/web-platform/tests/webusb/usbDevice_claimInterface-manual.https.html b/testing/web-platform/tests/webusb/usbDevice_claimInterface-manual.https.html
new file mode 100644
index 0000000000..991c1a9f31
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usbDevice_claimInterface-manual.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title></title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/manual.js"></script>
+ </head>
+ <body>
+ <p>
+ These tests require a USB device to be connected.
+ </p>
+ <script>
+ manual_usb_test(async (t, device) => {
+ await device.open();
+ const interfacesClaimed = [];
+ t.add_cleanup(async () => {
+ for (const iface of interfacesClaimed) {
+ await device.releaseInterface(iface.interfaceNumber);
+ }
+ await device.close();
+ });
+
+ await device.selectConfiguration(1);
+
+ const promises = [];
+ for (const iface of device.configuration.interfaces) {
+ const promise = device.claimInterface(iface.interfaceNumber);
+ promises.push(promise);
+
+ // Create a subtest for each interface so that success or failure to
+ // claim the interface is visible but does not affect the result of
+ // the overall test.
+ promise_test(async (t) => {
+ await promise;
+
+ interfacesClaimed.push(iface);
+ }, `Can claim interface ${iface.interfaceNumber}`);
+ }
+
+ await Promise.allSettled(promises);
+ }, 'claimInterface() resolves or rejects for all interfaces');
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/webusb/usbDevice_controlTransferIn-manual.https.html b/testing/web-platform/tests/webusb/usbDevice_controlTransferIn-manual.https.html
new file mode 100644
index 0000000000..c39e255e2b
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usbDevice_controlTransferIn-manual.https.html
@@ -0,0 +1,348 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title></title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/manual.js"></script>
+ </head>
+ <body>
+ <p>
+ These tests require a USB device to be connected.
+ </p>
+ <script>
+ const kGetDescriptorRequest = 0x06;
+
+ const kDeviceDescriptorType = 0x01;
+ const kDeviceDescriptorLength = 18;
+
+ const kConfigurationDescriptorType = 0x02;
+ const kConfigurationDescriptorLength = 9;
+
+ const kStringDescriptorType = 0x03;
+ const kStringDescriptorMaxLength = 0xFF;
+
+ const kInterfaceDescriptorType = 0x04;
+ const kInterfaceDescriptorLength = 9;
+
+ const kEndpointDescriptorType = 0x05;
+ const kEndpointDescriptorLength = 7;
+
+ let device = null;
+ let pending_subtests = 0;
+ const string_tests = new Set();
+ const string_languages = [];
+
+ async function subtest_complete() {
+ if (--pending_subtests == 0) {
+ await device.close();
+ }
+ }
+
+ function manual_usb_subtest(func, name, properties) {
+ pending_subtests++;
+ promise_test(async (test) => {
+ test.add_cleanup(subtest_complete);
+ await func(test);
+ }, name, properties);
+ }
+
+ function read_string(index, expected) {
+ // A device may use the same string in multiple places. Don't bother
+ // repeating the test.
+ if (string_tests.has(index)) {
+ return;
+ }
+ string_tests.add(index);
+
+ const string_values = new Set();
+ const decoder = new TextDecoder('utf-16le');
+
+ for (const language of string_languages) {
+ const language_string = language.toString(16).padStart(4, '0');
+ manual_usb_subtest(async (t) => {
+ if (expected != undefined) {
+ t.add_cleanup(() => {
+ if (string_values.size == string_languages.length) {
+ assert_true(string_values.has(expected));
+ }
+ });
+ }
+
+ const result = await device.controlTransferIn({
+ requestType: 'standard',
+ recipient: 'device',
+ request: kGetDescriptorRequest,
+ value: kStringDescriptorType << 8 | index,
+ index: language
+ }, kStringDescriptorMaxLength);
+
+ assert_equals(result.status, 'ok', 'transfer status');
+ const length = result.data.getUint8(0);
+ assert_greater_than_equal(length, 2, 'descriptor length');
+ assert_equals(result.data.byteLength, length, 'transfer length');
+ assert_equals(result.data.getUint8(1), kStringDescriptorType,
+ 'descriptor type');
+ const string_buffer = new Uint8Array(
+ result.data.buffer, result.data.byteOffset + 2, length - 2);
+ string_values.add(decoder.decode(string_buffer));
+ },
+ `Read string descriptor ${index} in language 0x${language_string}`);
+ }
+ }
+
+ function check_interface_descriptor(configuration, data) {
+ assert_greater_than_equal(
+ data.getUint8(0), kInterfaceDescriptorLength, 'descriptor length');
+
+ const interface_number = data.getUint8(2);
+ const iface = configuration.interfaces.find((iface) => {
+ return iface.interfaceNumber == interface_number;
+ });
+ assert_not_equals(
+ iface, undefined, `unknown interface ${interface_number}`);
+
+ const alternate_setting = data.getUint8(3);
+ const alternate = iface.alternates.find((alternate) => {
+ return alternate.alternateSetting == alternate_setting;
+ });
+ assert_not_equals(
+ alternate, undefined, `unknown alternate ${alternate_setting}`);
+
+ assert_equals(data.getUint8(4), alternate.endpoints.length,
+ 'number of endpoints');
+ assert_equals(
+ data.getUint8(5), alternate.interfaceClass, 'interface class');
+ assert_equals(data.getUint8(6), alternate.interfaceSubclass,
+ 'interface subclass');
+ assert_equals(data.getUint8(7), alternate.interfaceProtocol,
+ 'interface protocol');
+
+ const interface_string = data.getUint8(8);
+ if (interface_string != 0) {
+ // TODO(crbug.com/727819): Check that the string descriptor matches
+ // iface.interfaceName.
+ read_string(interface_string);
+ }
+
+ return alternate;
+ }
+
+ function check_endpoint_descriptor(alternate, data) {
+ assert_greater_than_equal(
+ data.getUint8(0), kEndpointDescriptorLength, 'descriptor length');
+
+ const endpoint_address = data.getUint8(2);
+ const direction = endpoint_address & 0x80 ? 'in' : 'out';
+ const endpoint_number = endpoint_address & 0x0f;
+ const endpoint = alternate.endpoints.find((endpoint) => {
+ return endpoint.direction == direction &&
+ endpoint.endpointNumber == endpoint_number;
+ });
+ assert_not_equals(
+ endpoint, undefined, `unknown endpoint ${endpoint_number}`);
+
+ const attributes = data.getUint8(3);
+ switch (attributes & 0x03) {
+ case 0:
+ assert_equals(endpoint.type, 'control', 'endpoint type');
+ break;
+ case 1:
+ assert_equals(endpoint.type, 'isochronous', 'endpoint type');
+ break;
+ case 2:
+ assert_equals(endpoint.type, 'bulk', 'endpoint type');
+ break;
+ case 3:
+ assert_equals(endpoint.type, 'interrupt', 'endpoint type');
+ break;
+ }
+
+ assert_equals(data.getUint16(4, /*littleEndian=*/true),
+ endpoint.packetSize, 'packet size');
+ }
+
+ function read_config_descriptor(config_value) {
+ manual_usb_subtest(async (t) => {
+ const configuration = device.configurations.find((config) => {
+ return config.configurationValue == config_value;
+ });
+ assert_not_equals(configuration, undefined);
+
+ let result = await device.controlTransferIn({
+ requestType: 'standard',
+ recipient: 'device',
+ request: kGetDescriptorRequest,
+ value: kConfigurationDescriptorType << 8 | (config_value - 1),
+ index: 0
+ }, kConfigurationDescriptorLength);
+
+ assert_equals(result.status, 'ok', 'transfer status');
+ let length = result.data.getUint8(0);
+ assert_greater_than_equal(
+ length, kConfigurationDescriptorLength, 'descriptor length');
+ assert_equals(result.data.byteLength, length, 'transfer length');
+ const total_length = result.data.getUint16(2, /*littleEndian=*/true);
+
+ result = await device.controlTransferIn({
+ requestType: 'standard',
+ recipient: 'device',
+ request: kGetDescriptorRequest,
+ value: kConfigurationDescriptorType << 8 | (config_value - 1),
+ index: 0
+ }, total_length);
+
+ assert_equals(result.status, 'ok', 'transfer status');
+ assert_equals(
+ result.data.byteLength, total_length, 'transfer length');
+ assert_equals(result.data.getUint8(0), length, 'descriptor length');
+ assert_equals(result.data.getUint8(1), kConfigurationDescriptorType,
+ 'descriptor type');
+ assert_equals(result.data.getUint16(2, /*littleEndian=*/true),
+ total_length, 'total length');
+ assert_equals(
+ result.data.getUint8(4), configuration.interfaces.length,
+ 'number of interfaces');
+ assert_equals(
+ result.data.getUint8(5), config_value, 'configuration value');
+
+ const configuration_string = result.data.getUint8(6);
+ if (configuration_string != 0) {
+ // TODO(crbug.com/727819): Check that the string descriptor matches
+ // configuration.configurationName.
+ read_string(configuration_string);
+ }
+
+ let offset = length;
+ let alternate = undefined;
+ while (offset < total_length) {
+ length = result.data.getUint8(offset);
+ assert_less_than_equal(offset + length, total_length);
+
+ const view = new DataView(
+ result.data.buffer, result.data.byteOffset + offset, length);
+ switch (view.getUint8(1)) {
+ case kConfigurationDescriptorType:
+ assert_unreached('cannot contain multiple config descriptors');
+ break;
+ case kInterfaceDescriptorType:
+ alternate = check_interface_descriptor(configuration, view);
+ break;
+ case kEndpointDescriptorType:
+ assert_not_equals(alternate, undefined,
+ 'endpoint not defined after interface');
+ check_endpoint_descriptor(alternate, view);
+ break;
+ }
+
+ offset += length;
+ }
+ }, `Read config descriptor ${config_value}`);
+ }
+
+ function read_string_descriptor_languages(device_descriptor) {
+ manual_usb_subtest(async (t) => {
+ const result = await device.controlTransferIn({
+ requestType: 'standard',
+ recipient: 'device',
+ request: kGetDescriptorRequest,
+ value: kStringDescriptorType << 8,
+ index: 0
+ }, kStringDescriptorMaxLength);
+
+ assert_equals(result.status, 'ok', 'transfer status');
+ assert_equals(result.data.getUint8(1), kStringDescriptorType,
+ 'descriptor type');
+ const length = result.data.getUint8(0);
+ assert_greater_than_equal(length, 2, 'descriptor length')
+ assert_greater_than_equal(
+ result.data.byteLength, length, 'transfer length');
+
+ for (let index = 2; index < length; index += 2) {
+ string_languages.push(
+ result.data.getUint16(index, /*littleEndian=*/true));
+ }
+
+ const manufacturer_string = device_descriptor.getUint8(14);
+ if (manufacturer_string != 0) {
+ assert_not_equals(device.manufacturerName, undefined);
+ read_string(manufacturer_string, device.manufacturerName);
+ }
+
+ const product_string = device_descriptor.getUint8(15);
+ if (product_string != 0) {
+ assert_not_equals(device.productName, undefined);
+ read_string(product_string, device.productName);
+ }
+
+ const serial_number_string = device_descriptor.getUint8(16);
+ if (serial_number_string != 0) {
+ assert_not_equals(device.serialNumber, undefined);
+ read_string(serial_number_string, device.serialNumber);
+ }
+
+ const num_configurations = device_descriptor.getUint8(17);
+ for (let config_value = 1; config_value <= num_configurations;
+ ++config_value) {
+ read_config_descriptor(config_value);
+ }
+ }, `Read supported languages`);
+ }
+
+ promise_test(async (t) => {
+ device = await getDeviceForManualTest();
+ await device.open();
+
+ const result = await device.controlTransferIn({
+ requestType: 'standard',
+ recipient: 'device',
+ request: kGetDescriptorRequest,
+ value: kDeviceDescriptorType << 8,
+ index: 0,
+ }, kDeviceDescriptorLength);
+
+ assert_equals(result.status, 'ok', 'transfer status');
+ assert_equals(
+ result.data.byteLength, kDeviceDescriptorLength, 'transfer length');
+ assert_greater_than_equal(
+ result.data.getUint8(0),
+ kDeviceDescriptorLength, 'descriptor length');
+ assert_equals(result.data.getUint8(1), kDeviceDescriptorType,
+ 'descriptor type');
+ const bcd_usb = result.data.getUint16(2, /*littleEndian=*/true);
+ assert_equals(
+ bcd_usb >> 8, device.usbVersionMajor, 'USB version major');
+ assert_equals(
+ (bcd_usb & 0xf0) >> 4, device.usbVersionMinor, 'USB version minor');
+ assert_equals(
+ bcd_usb & 0xf, device.usbVersionSubminor, 'USV version subminor');
+ assert_equals(
+ result.data.getUint8(4), device.deviceClass, 'device class');
+ assert_equals(
+ result.data.getUint8(5), device.deviceSubclass, 'device subclass');
+ assert_equals(
+ result.data.getUint8(6), device.deviceProtocol, 'device protocol');
+ assert_equals(result.data.getUint16(8, /*littleEndian=*/true),
+ device.vendorId, 'vendor id');
+ assert_equals(result.data.getUint16(10, /*littleEndian=*/true),
+ device.productId, 'product id');
+ const bcd_device = result.data.getUint16(12, /*littleEndian=*/true);
+ assert_equals(
+ bcd_device >> 8, device.deviceVersionMajor, 'device version major');
+ assert_equals((bcd_device & 0xf0) >> 4, device.deviceVersionMinor,
+ 'device version minor');
+ assert_equals(bcd_device & 0xf, device.deviceVersionSubminor,
+ 'device version subminor');
+ assert_equals(result.data.getUint8(17), device.configurations.length,
+ 'number of configurations');
+
+ read_string_descriptor_languages(result.data);
+
+ if (pending_subtests == 0) {
+ await device.close();
+ }
+ }, 'Read device descriptor');
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webusb/usbDevice_forget-manual.https.html b/testing/web-platform/tests/webusb/usbDevice_forget-manual.https.html
new file mode 100644
index 0000000000..9b50852454
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usbDevice_forget-manual.https.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title></title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/manual.js"></script>
+ </head>
+ <body>
+ <p>
+ These tests require a USB device to be connected.
+ </p>
+ <script>
+ manual_usb_test(async (t, device) => {
+ const devicesBeforeForget = await navigator.usb.getDevices();
+ assert_equals(devicesBeforeForget.length, 1);
+ assert_equals(devicesBeforeForget[0], device);
+
+ await device.forget();
+
+ const devicesAfterForget = await navigator.usb.getDevices();
+ assert_equals(devicesAfterForget.length, 0);
+ }, 'forget() removes the device from getDevices()');
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/webusb/usbDevice_reset-manual.https.html b/testing/web-platform/tests/webusb/usbDevice_reset-manual.https.html
new file mode 100644
index 0000000000..63a0c356ee
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usbDevice_reset-manual.https.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title></title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/manual.js"></script>
+ </head>
+ <body>
+ <p>
+ These tests require a USB device to be connected.
+ </p>
+ <script>
+ const kGetDescriptorRequest = 0x06;
+ const kDeviceDescriptorType = 0x01;
+ const kDeviceDescriptorLength = 18;
+
+ manual_usb_test(async (t, device) => {
+ await device.open();
+ t.add_cleanup(async () => {
+ await device.close();
+ });
+
+ // This test exercises the behavior that the device remains open when it
+ // is reset. If the device changes its properties too drastically when
+ // reset it may appear to disconnect instead.
+ await device.reset();
+
+ // Read the device descriptor in order to validate that communication
+ // with the device is still possible after a reset.
+ const result = await device.controlTransferIn({
+ requestType: 'standard',
+ recipient: 'device',
+ request: kGetDescriptorRequest,
+ value: kDeviceDescriptorType << 8,
+ index: 0,
+ }, kDeviceDescriptorLength);
+
+ assert_equals(result.status, 'ok', 'transfer status');
+ assert_equals(
+ result.data.byteLength, kDeviceDescriptorLength, 'transfer length');
+ }, 'reset() does not disconnect the device');
+ </script>
+ </body>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/webusb/usbDevice_transferIn-manual.https.html b/testing/web-platform/tests/webusb/usbDevice_transferIn-manual.https.html
new file mode 100644
index 0000000000..c0fad37e20
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usbDevice_transferIn-manual.https.html
@@ -0,0 +1,148 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title></title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/manual.js"></script>
+ </head>
+ <body>
+ <p>
+ This test requires a USB device implementing the USB CDC-ACM protocol
+ configured to loop back TX to RX. For example, this Arduino sketch could
+ be used:
+
+ <pre>
+void setup() {
+ Serial.begin(115200);
+ Serial.setTimeout(0);
+ while (!Serial) {
+ ;
+ }
+}
+
+void loop() {
+ if (Serial.available()) {
+ char buf[1024]; // Greater than the endpoint packet size.
+ int count = Serial.readBytes(buf, sizeof buf);
+ Serial.write(buf, count);
+ }
+}
+ </pre>
+ </p>
+ <script>
+ manual_usb_serial_test(async (t, device, inEndpoint, outEndpoint) => {
+ // Set up two IN transfers which should complete in order.
+ const transfer1 =
+ device.transferIn(inEndpoint.endpointNumber, inEndpoint.packetSize);
+ const transfer2 =
+ device.transferIn(inEndpoint.endpointNumber, inEndpoint.packetSize);
+
+ // Write a single byte to the port which should be echoed to complete
+ // transfer1.
+ let result = await device.transferOut(
+ outEndpoint.endpointNumber, new Uint8Array(['a'.charCodeAt(0)]));
+ assert_equals(result.status, 'ok');
+ assert_equals(result.bytesWritten, 1);
+
+ result = await transfer1;
+ assert_equals(result.status, 'ok');
+ assert_not_equals(result.data, null);
+ assert_equals(result.data.byteLength, 1, 'byteLength');
+ assert_equals(result.data.getUint8(0), 'a'.charCodeAt(0));
+
+ // Set up a third IN transfer which will be canceled when the device is
+ // closed at the end of the test.
+ const transfer3 = promise_rejects_dom(
+ t, 'AbortError',
+ device.transferIn(inEndpoint.endpointNumber,
+ inEndpoint.packetSize));
+
+ // Write a single byte to the port which should be echoed to complete
+ // transfer2.
+ result = await device.transferOut(
+ outEndpoint.endpointNumber, new Uint8Array(['b'.charCodeAt(0)]));
+ assert_equals(result.status, 'ok');
+ assert_equals(result.bytesWritten, 1);
+
+ result = await transfer2;
+ assert_equals(result.status, 'ok');
+ assert_not_equals(result.data, null);
+ assert_equals(result.data.byteLength, 1, 'byteLength');
+ assert_equals(result.data.getUint8(0), 'b'.charCodeAt(0));
+
+ await device.close();
+ await transfer3;
+ }, 'Multiple small IN transfers on an endpoint complete in order');
+
+ manual_usb_serial_test(async (t, device, inEndpoint, outEndpoint) => {
+ const bufferLength = outEndpoint.packetSize * 20;
+ const parallelRequests = 6;
+
+ // Keep track of the order in which transfers are submitted.
+ let enqueueSequence = 0;
+ let dequeueSequence = 0;
+ const received = new Uint8Array(bufferLength);
+ let receivedOffset = 0;
+ let done = false;
+ const transfers = [];
+
+ async function readNext(sequence) {
+ let result;
+ try {
+ result = await device.transferIn(inEndpoint.endpointNumber,
+ inEndpoint.packetSize);
+ } catch (e) {
+ // The last few transfers will fail when the device is closed.
+ assert_true(done);
+ assert_equals(dequeueSequence++, sequence, 'dequeueSequence done');
+ assert_equals(receivedOffset, bufferLength, 'receivedOffset');
+ assert_equals(e.name, 'AbortError');
+ return;
+ }
+
+ assert_equals(dequeueSequence++, sequence, 'dequeueSequence');
+ assert_equals(result.status, 'ok');
+ assert_not_equals(result.data, null);
+
+ const data = new Uint8Array(
+ result.data.buffer, result.data.byteOffset,
+ result.data.byteLength);
+ received.set(data, receivedOffset);
+ receivedOffset += result.data.byteLength;
+
+ // Check |done| because there might be zero-length packet completions
+ // after the data has been completely received.
+ if (!done) {
+ if (receivedOffset == bufferLength) {
+ done = true;
+ assert_array_equals(received, buffer);
+ await device.close();
+ } else {
+ await readNext(enqueueSequence++);
+ }
+ }
+ }
+
+ for (let i = 0; i < parallelRequests; ++i) {
+ transfers.push(readNext(enqueueSequence++));
+ }
+
+ // Write a large buffer to the device which will be split up into
+ // smaller packets when echoed back.
+ const buffer = new Uint8Array(bufferLength);
+ for (let i = 0; i < buffer.byteLength; ++i) {
+ buffer[i] = i;
+ }
+ let result = await device.transferOut(
+ outEndpoint.endpointNumber, buffer);
+ assert_equals(result.status, 'ok');
+ assert_equals(result.bytesWritten, buffer.byteLength);
+
+ await Promise.all(transfers);
+ assert_equals(dequeueSequence, enqueueSequence);
+ }, 'Multiple large IN transfers on an endpoint complete in order');
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/webusb/usbEndpoint.https.any.js b/testing/web-platform/tests/webusb/usbEndpoint.https.any.js
new file mode 100644
index 0000000000..c987e4c333
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usbEndpoint.https.any.js
@@ -0,0 +1,46 @@
+// META: script=/resources/test-only-api.js
+// META: script=/webusb/resources/fake-devices.js
+// META: script=/webusb/resources/usb-helpers.js
+'use strict';
+
+usb_test(async () => {
+ let { device } = await getFakeDevice();
+ let configuration = new USBConfiguration(
+ device, device.configurations[1].configurationValue);
+ let usbInterface = new USBInterface(
+ configuration, configuration.interfaces[0].interfaceNumber);
+ let alternateInterface = new USBAlternateInterface(
+ usbInterface, usbInterface.alternates[1].alternateSetting);
+ let inEndpoint = new USBEndpoint(
+ alternateInterface, alternateInterface.endpoints[0].endpointNumber, 'in');
+ let outEndpoint = new USBEndpoint(
+ alternateInterface,
+ alternateInterface.endpoints[1].endpointNumber,
+ 'out');
+ assertDeviceInfoEquals(
+ inEndpoint,
+ fakeDeviceInit.configurations[1].interfaces[0].alternates[1]
+ .endpoints[0]);
+ assertDeviceInfoEquals(
+ outEndpoint,
+ fakeDeviceInit.configurations[1].interfaces[0].alternates[1]
+ .endpoints[1]);
+}, 'Can construct a USBEndpoint.');
+
+usb_test(async () => {
+ let { device } = await getFakeDevice();
+ let configuration = new USBConfiguration(
+ device, device.configurations[1].configurationValue);
+ let usbInterface = new USBInterface(
+ configuration, configuration.interfaces[0].interfaceNumber);
+ let alternateInterface = new USBAlternateInterface(
+ usbInterface, usbInterface.alternates[1].alternateSetting);
+ try {
+ let endpoint = new USBEndpoint(
+ alternateInterface, alternateInterface.endpoints.length, 'in');
+ assert_unreached('USBEndpoint should reject an invalid endpoint number');
+ } catch (error) {
+ assert_equals(error.name, 'RangeError');
+ }
+}, 'Constructing a USBEndpoint with an invalid endpoint number throws a ' +
+ 'range error.');
diff --git a/testing/web-platform/tests/webusb/usbInTransferResult.https.any.js b/testing/web-platform/tests/webusb/usbInTransferResult.https.any.js
new file mode 100644
index 0000000000..dcfa38124a
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usbInTransferResult.https.any.js
@@ -0,0 +1,29 @@
+// META: script=/resources/test-only-api.js
+// META: script=/webusb/resources/fake-devices.js
+// META: script=/webusb/resources/usb-helpers.js
+'use strict';
+
+test(t => {
+ let data_view = new DataView(Uint8Array.from([1, 2, 3, 4]).buffer);
+ let result = new USBInTransferResult('ok', data_view);
+ assert_equals(result.status, 'ok');
+ assert_equals(result.data.getInt32(0), 16909060);
+}, 'Can construct a USBInTransferResult');
+
+test(t => {
+ let result = new USBInTransferResult('stall');
+ assert_equals(result.status, 'stall');
+ assert_equals(result.data, null);
+
+ result = new USBInTransferResult('babble', null);
+ assert_equals(result.status, 'babble');
+ assert_equals(result.data, null);
+}, 'Can construct a USBInTransferResult without a DataView');
+
+test(t => {
+ assert_throws_js(TypeError, () => new USBInTransferResult('invalid_status'));
+}, 'Cannot construct USBInTransferResult with an invalid status');
+
+test(t => {
+ assert_throws_js(TypeError, () => new USBInTransferResult());
+}, 'Cannot construct USBInTransferResult without a status');
diff --git a/testing/web-platform/tests/webusb/usbInterface.https.any.js b/testing/web-platform/tests/webusb/usbInterface.https.any.js
new file mode 100644
index 0000000000..22692a7d94
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usbInterface.https.any.js
@@ -0,0 +1,55 @@
+// META: script=/resources/test-only-api.js
+// META: script=/webusb/resources/fake-devices.js
+// META: script=/webusb/resources/usb-helpers.js
+'use strict';
+
+usb_test(async () => {
+ let { device } = await getFakeDevice();
+ let configuration = new USBConfiguration(
+ device, device.configurations[1].configurationValue);
+ let usbInterface = new USBInterface(
+ configuration, configuration.interfaces[0].interfaceNumber);
+ assertDeviceInfoEquals(
+ usbInterface, fakeDeviceInit.configurations[1].interfaces[0]);
+}, 'Can construct a USBInterface.');
+
+usb_test(async () => {
+ let { device } = await getFakeDevice();
+ let configuration = new USBConfiguration(
+ device, device.configurations[1].configurationValue);
+ try {
+ let usbInterface = new USBInterface(
+ configuration, configuration.interfaces.length);
+ assert_unreached('USBInterface should reject an invalid interface number');
+ } catch (error) {
+ assert_equals(error.name, 'RangeError');
+ }
+}, 'Constructing a USBInterface with an invalid interface number ' +
+ 'throws a range error.');
+
+usb_test(async () => {
+ let { device } = await getFakeDevice();
+ await device.open();
+ await device.selectConfiguration(2);
+ let configuration = new USBConfiguration(
+ device, device.configurations[1].configurationValue);
+ let usbInterface = new USBInterface(
+ configuration, configuration.interfaces[0].interfaceNumber);
+ assert_equals(usbInterface.alternate.alternateSetting, 0);
+}, 'The alternate attribute of USBInterface returns the one with ' +
+ 'bAlternateSetting 0 if the interface has not been claimed.');
+
+usb_test(async () => {
+ let { device } = await getFakeDevice();
+ await device.open();
+ await device.selectConfiguration(2);
+ await device.claimInterface(0);
+ let configuration = new USBConfiguration(
+ device, device.configurations[1].configurationValue);
+ let usbInterface = new USBInterface(
+ configuration, configuration.interfaces[0].interfaceNumber);
+ assert_equals(usbInterface.alternate.alternateSetting, 0);
+ await device.selectAlternateInterface(0, 1);
+ assert_equals(usbInterface.alternate.alternateSetting, 1);
+}, 'The alternate attribute of USBInterface returns the active alternate ' +
+ 'interface.');
diff --git a/testing/web-platform/tests/webusb/usbIsochronousInTransferPacket.https.any.js b/testing/web-platform/tests/webusb/usbIsochronousInTransferPacket.https.any.js
new file mode 100644
index 0000000000..55543d11f8
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usbIsochronousInTransferPacket.https.any.js
@@ -0,0 +1,28 @@
+'use strict';
+
+test(t => {
+ let data_view = new DataView(Uint8Array.from([1, 2, 3, 4]).buffer);
+ let packet = new USBIsochronousInTransferPacket('ok', data_view);
+ assert_equals(packet.status, 'ok');
+ assert_equals(packet.data.getInt32(0), 16909060);
+}, 'Can construct a USBIsochronousInTransferPacket');
+
+test(t => {
+ let packet = new USBIsochronousInTransferPacket('stall');
+ assert_equals(packet.status, 'stall');
+ assert_equals(packet.data, null);
+
+ packet = new USBIsochronousInTransferPacket('stall', null);
+ assert_equals(packet.status, 'stall');
+ assert_equals(packet.data, null);
+}, 'Can construct a USBIsochronousInTransferPacket without a DataView');
+
+test(t => {
+ assert_throws_js(TypeError, () => {
+ new USBIsochronousInTransferPacket('invalid_status');
+ });
+}, 'Cannot construct USBIsochronousInTransferPacket with an invalid status');
+
+test(t => {
+ assert_throws_js(TypeError, () => new USBIsochronousInTransferPacket());
+}, 'Cannot construct USBIsochronousInTransferPacket without a status');
diff --git a/testing/web-platform/tests/webusb/usbIsochronousInTransferResult.https.any.js b/testing/web-platform/tests/webusb/usbIsochronousInTransferResult.https.any.js
new file mode 100644
index 0000000000..0aa57d00e6
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usbIsochronousInTransferResult.https.any.js
@@ -0,0 +1,36 @@
+'use strict';
+
+test(t => {
+ let data_view = new DataView(Uint8Array.from([1, 2, 3, 4]).buffer);
+ let packet_data_view = new DataView(data_view.buffer);
+ let packets = [
+ new USBIsochronousInTransferPacket('ok', packet_data_view),
+ new USBIsochronousInTransferPacket('stall')
+ ];
+
+ let result = new USBIsochronousInTransferResult(packets, data_view);
+ assert_equals(result.data.getInt32(0), 16909060);
+ assert_equals(result.packets.length, 2);
+ assert_equals(result.packets[0].status, 'ok');
+ assert_equals(result.packets[0].data.getInt32(0), 16909060);
+ assert_equals(result.packets[1].status, 'stall');
+ assert_equals(result.packets[1].data, null);
+}, 'Can construct a USBIsochronousInTransferResult');
+
+test(t => {
+ let packets = [
+ new USBIsochronousInTransferPacket('stall'),
+ new USBIsochronousInTransferPacket('stall')
+ ];
+ let result = new USBIsochronousInTransferResult(packets);
+ assert_equals(result.data, null);
+ assert_equals(result.packets.length, 2);
+ assert_equals(result.packets[0].status, 'stall');
+ assert_equals(result.packets[0].data, null);
+ assert_equals(result.packets[1].status, 'stall');
+ assert_equals(result.packets[1].data, null);
+}, 'Can construct a USBIsochronousInTransferResult without a DataView');
+
+test(t => {
+ assert_throws_js(TypeError, () => new USBIsochronousInTransferResult());
+}, 'Cannot construct a USBIsochronousInTransferResult without packets');
diff --git a/testing/web-platform/tests/webusb/usbIsochronousOutTransferPacket.https.any.js b/testing/web-platform/tests/webusb/usbIsochronousOutTransferPacket.https.any.js
new file mode 100644
index 0000000000..2747a6fa0a
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usbIsochronousOutTransferPacket.https.any.js
@@ -0,0 +1,21 @@
+'use strict';
+
+test(t => {
+ let packet = new USBIsochronousOutTransferPacket('ok', 42);
+ assert_equals(packet.status, 'ok');
+ assert_equals(packet.bytesWritten, 42);
+
+ packet = new USBIsochronousOutTransferPacket('stall');
+ assert_equals(packet.status, 'stall');
+ assert_equals(packet.bytesWritten, 0);
+}, 'Can construct USBIsochronousOutTransferPacket');
+
+test(t => {
+ assert_throws_js(TypeError, () => {
+ new USBIsochronousOutTransferPacket('invalid_status');
+ });
+}, 'Cannot construct USBIsochronousOutTransferPacket with an invalid status');
+
+test(t => {
+ assert_throws_js(TypeError, () => new USBIsochronousOutTransferPacket());
+}, 'Cannot construct USBIsochronousOutTransferPacket without a status');
diff --git a/testing/web-platform/tests/webusb/usbIsochronousOutTransferResult.https.any.js b/testing/web-platform/tests/webusb/usbIsochronousOutTransferResult.https.any.js
new file mode 100644
index 0000000000..692420d948
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usbIsochronousOutTransferResult.https.any.js
@@ -0,0 +1,19 @@
+'use strict';
+
+test(t => {
+ let packets = [
+ new USBIsochronousOutTransferPacket('ok', 42),
+ new USBIsochronousOutTransferPacket('stall')
+ ];
+
+ let result = new USBIsochronousOutTransferResult(packets);
+ assert_equals(result.packets.length, 2);
+ assert_equals(result.packets[0].status, 'ok');
+ assert_equals(result.packets[0].bytesWritten, 42);
+ assert_equals(result.packets[1].status, 'stall');
+ assert_equals(result.packets[1].bytesWritten, 0);
+}, 'Can construct a USBIsochronousOutTransferResult');
+
+test(t => {
+ assert_throws_js(TypeError, () => new USBIsochronousOutTransferResult());
+}, 'Cannot construct a USBIsochronousOutTransferResult without packets');
diff --git a/testing/web-platform/tests/webusb/usbOutTransferResult.https.any.js b/testing/web-platform/tests/webusb/usbOutTransferResult.https.any.js
new file mode 100644
index 0000000000..200c0716eb
--- /dev/null
+++ b/testing/web-platform/tests/webusb/usbOutTransferResult.https.any.js
@@ -0,0 +1,19 @@
+'use strict';
+
+test(t => {
+ let result = new USBOutTransferResult('ok', 42);
+ assert_equals(result.status, 'ok');
+ assert_equals(result.bytesWritten, 42);
+
+ result = new USBOutTransferResult('stall');
+ assert_equals(result.status, 'stall');
+ assert_equals(result.bytesWritten, 0);
+}, 'Can construct USBOutTransferResult');
+
+test(t => {
+ assert_throws_js(TypeError, () => new USBOutTransferResult('invalid_status'));
+}, 'Cannot construct USBOutTransferResult with an invalid status');
+
+test(t => {
+ assert_throws_js(TypeError, () => new USBOutTransferResult());
+}, 'Cannot construct USBOutTransferResult without a status');