diff options
Diffstat (limited to 'testing/web-platform/tests/webusb')
45 files changed, 3384 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/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/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..730db24902 --- /dev/null +++ b/testing/web-platform/tests/webusb/resources/open-in-iframe.html @@ -0,0 +1,19 @@ +<!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> +'use strict'; +window.onmessage = messageEvent => { + if (messageEvent.data === 'Ready') { + navigator.usb.addEventListener('connect', connectEvent => { + connectEvent.device.open().then(() => { + parent.postMessage('Success', '*'); + }).catch(error => { + parent.postMessage('FAIL: open rejected ' + error, '*'); + }); + }); + parent.postMessage('Ready', '*'); + } +}; +</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..e16621057a --- /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 === 'Ready') { + 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..5a6dd0565f --- /dev/null +++ b/testing/web-platform/tests/webusb/usbDevice-iframe.https.html @@ -0,0 +1,45 @@ +<!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 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('Ready', '*'); + + 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.'); +</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..dcad6ec08e --- /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: 'Ready' }); + + 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'); |