1. Introduction
This section is non-normative.
Standards such as [WebUSB] pose a challenge to test authors because to fully exercise their interfaces requires physical hardware devices that respond in predictable ways. To address this challenge this specification defines an interface for controlling a simulation of the USB subsystem on the host the User Agent (UA) is running on. With this interface devices with particular properties can be created and their responses to requests are well defined.
The purpose if this interface is to assist the developers of UAs and so this interface does not permit arbitrary control over the behavior of devices. Some parameters are configurable and some are specified.
The only intended client of this API are tests in Web Platform Tests. This testing API will be changed as needed to support the evolution of those tests.
Testing applications that use WebUSB is specifically not in scope for this API. It not designed for that purpose. And, the ability to make breaking changes to this API has higher priority.
1.1. Examples
let fakeDeviceInit = {
usbVersionMajor: 2,
usbVersionMinor: 0,
usbVersionSubminor: 0,
deviceClass: 0xFF,
deviceSubclass: 0xFF,
deviceProtocol: 0xFF,
vendorId: 0x1234,
productId: 0xABCD,
deviceVersionMajor: 1,
deviceVersionMinor: 0,
deviceVersionSubminor: 0,
configurations: []
};
function promiseForEvent(eventTarget, eventType) {
return new Promise(resolve => {
let eventHandler = evt => {
resolve(evt);
eventTarget.removeEventListener(eventTarget, eventHandler);
};
eventTarget.addEventListener(eventType);
});
}
promise_test(async () => {
await navigator.usb.test.initialize();
let fakeDevice = navigator.usb.addFakeDevice(fakeDeviceInit);
let connectEvent = await promiseForEvent(navigator.usb, 'connect');
let device = connectEvent.device;
assert_equals(device.usbVersionMajor, fakeDeviceInit.usbVersionMajor);
assert_equals(device.usbVersionMinor, fakeDeviceInit.usbVersionMinor);
assert_equals(device.usbVersionSubminor, fakeDeviceInit.usbVersionSubminor);
assert_equals(device.deviceClass, fakeDeviceInit.deviceClass);
assert_equals(device.deviceSubclass, fakeDeviceInit.deviceSubclass);
assert_equals(device.deviceProtocol, fakeDeviceInit.deviceProtocol);
assert_equals(device.vendorId, fakeDeviceInit.vendorId);
assert_equals(device.productId, fakeDeviceInit.productId);
assert_equals(device.deviceVersionMajor, fakeDeviceInit.deviceVersionMajor);
assert_equals(device.deviceVersionMinor, fakeDeviceInit.deviceVersionMinor);
assert_equals(device.deviceVersionSubminor, fakeDeviceInit.deviceVersionSubminor);
assert_equals(device.configuration, null);
assert_equals(device.configurations.length, 0);
let devices = await navigator.usb.getDevices();
assert_array_equals(devices, [device]);
fakeDevice.disconnect();
let disconnectEvent = await promiseForEvent(navigator.usb, 'disconnect');
assert_equals(disconnectEvent.device, device);
});
navigator.usb.requestDevice() method.
promise_test(async () => {
await navigator.usb.test.initialize();
let fakeDevice = navigator.usb.addFakeDevice(fakeDeviceInit);
let connectEvent = await promiseForEvent(navigator.usb, 'connect');
navigator.usb.test.onrequestdevice = () => connectEvent.device;
let options = { filters: [{ vendorId: 0x1234 }] };
let device = await navigator.usb.requestDevice(options);
assert_array_equals(navigator.usb.test.lastFilters, options.filters);
assert_equals(device, connectEvent.device);
}
Note: If multiple tests are run in the same document then the test harness
should invoke reset() and wait until the Promise it returns is
resolved before executing the next test.
2. Availability
This specification defines an interface that is not intended be used by non-testing-related web content. The UA MAY choose to expose this interface only when a runtime or compile-time flag has been set.
-
The risk of introducing a security vulnerability in the default configuration is mitigated.
-
The polyfill is not shipped with the production application and so there is no increase in binary size to support an API that will rarely be used.
3. Global Testing Interface
partial interface USB { [SameObject ]readonly attribute USBTest ; }; [test Exposed =(Window ,Worker )]interface :USBDeviceRequestEvent Event {attribute FrozenArray <USBDeviceFilter >;filters undefined (respondWith Promise <FakeUSBDevice >); }; [result Exposed =(Window ,Worker )]interface {USBTest attribute EventHandler ;onrequestdevice Promise <undefined >();initialize Promise <undefined >((attachToContext HTMLIFrameElement or Worker ));context FakeUSBDevice (addFakeDevice FakeUSBDeviceInit );deviceInit Promise <undefined >(); };reset
By default, a UA SHALL NOT alter the behavior of a USB instance usb in any global object until it is reconfigured so that usb is controlled
by a USBTest instance test. At that point the behavior of usb is
defined by this specification.
Instances of USBTest are created with an internal slot [[initializationPromise]] with an initial
value of null.
When invoked, the initialize() method MUST run these steps:
-
Let test be the
USBTestinstance on which this method was invoked. -
If test.
[[initializationPromise]]isnullthen set test.[[initializationPromise]]to a newPromiseand run these sub-steps in parallel:-
Reconfigure the UA’s internal implementation of the
usbobject in the current global object so that it is controlled by test.
-
-
Return test.
[[initializationPromise]].
When invoked, the attachToContext(context) method MUST return a new Promise promise and run these steps in parallel:
-
Let test be the
USBTestinstance on which this method was invoked. -
If test.
[[initializationPromise]]is not in the resolved state, reject promise with anInvalidStateErrorand abort these steps. -
Reconfigure the UA’s internal implementation of
usbin the global object associated with context so that it is controlled by test. -
Resolve promise.
When invoked, the addFakeDevice(deviceInit) method MUST run these
steps:
-
Let test be the
USBTestinstance on which this method was invoked. -
If test.
[[initializationPromise]]is not in the resolved state, raise anInvalidStateErrorand abort these steps. -
Let fakeDevice be a new
FakeUSBDevice. -
Queue a task to, for each
USBinstance controlled by test, perform the steps described in [WebUSB] for handling a new device that is connected to the system. -
Return fakeDevice.
When invoked, the reset() method MUST return a new Promise promise and run the following steps in parallel:
-
Let test be the
USBTestinstance on which this method was invoked. -
For each
FakeUSBDevicefakeDevice previously returned byaddFakeDevice(), invoke fakeDevice.disconnect(). -
Resolve promise.
USBDeviceRequestEvent instances have an internal slot [[promise]] that holds a Promise.
When invoked, the respondWith(result) method MUST run
the following steps in parallel:
-
Wait until result settles.
-
Let event be the
USBDeviceRequestEventinstance on which this method was invoked. -
If result resolved with response and response is an instance of
FakeUSBDeviceresolve event@[[promise]]with device. -
Otherwise, reject event@
[[promise]]with aNotFoundError.
3.1. USB Behavior
When requestDevice(options) is invoked on a USB instance controlled by a USBTest test the UA MUST return a new Promise promise and perform the following steps in parallel:
-
Let event be a new
USBDeviceRequestEvent. -
Set event.filters to a new
FrozenArray. -
Copy the members of options.
filtersinto event.filters. -
Set event@
[[promise]]to promise. -
Fire an event named requestdevice on test, using event as the event object.
3.2. Events
requestdevice: Fired on a USBTest object when requestDevice(options) is invoked after initialize() has
been called.
4. Fake Devices
To permit testing without physical hardware this specification defines a method
for tests to add simulated USB devices by calling addFakeDevice() with an instance of FakeUSBDeviceInit containing the properties of the
device to be added.
[Exposed =(Window ,Worker )]interface :FakeUSBDevice EventTarget {attribute EventHandler ;onclose undefined (); };disconnect dictionary {FakeUSBDeviceInit required octet ;usbVersionMajor required octet ;usbVersionMinor required octet ;usbVersionSubminor required octet ;deviceClass required octet ;deviceSubclass required octet ;deviceProtocol required unsigned short ;vendorId required unsigned short ;productId required octet ;deviceVersionMajor required octet ;deviceVersionMinor required octet ;deviceVersionSubminor DOMString ?;manufacturerName DOMString ?;productName DOMString ?;serialNumber octet = 0;activeConfigurationValue sequence <FakeUSBConfigurationInit >; };configurations dictionary {FakeUSBConfigurationInit required octet ;configurationValue DOMString ?;configurationName sequence <FakeUSBInterfaceInit >; };interfaces dictionary {FakeUSBInterfaceInit required octet ;interfaceNumber sequence <FakeUSBAlternateInterfaceInit >; };alternates dictionary {FakeUSBAlternateInterfaceInit required octet ;alternateSetting required octet ;interfaceClass required octet ;interfaceSubclass required octet ;interfaceProtocol DOMString ?;interfaceName sequence <FakeUSBEndpointInit >; };endpoints dictionary {FakeUSBEndpointInit required octet ;endpointNumber required USBDirection ;direction required USBEndpointType ;type required unsigned long ; };packetSize
When a USBDevice device is initialized from an fake USB device described
by a FakeUSBDeviceInit init passed to addFakeDevice() the
attributes of device SHALL be initialized as follows and device will correspond to the FakeUSBDevice returned by addFakeDevice():
-
For each non-sequence attribute of init other than
activeConfigurationValuethe attribute of device with the same name SHALL be set to its value. -
For each sequence of
FakeUSBConfigurationInit,FakeUSBInterfaceInitFakeUSBAlternateInterfaceInitandFakeUSBEndpointInitobjects correspondingUSBConfiguration,USBInterface,USBAlternateInterfaceandUSBEndpointobjects SHALL be created by similarly copying attributes with the same names and be used to build an identical hierarchy of objects in theconfigurations,interfaces,alternatesandendpointsFrozenArrays. -
If a
USBConfigurationinstance config withconfigurationValueequal to init.activeConfigurationValuethen device.configurationSHALL be set to config, otherwisenull.
When invoked, the disconnect() method MUST, queue a task to, for each USB instance controlled by the USBTest instance from which target of this invocation was returned, perform
the steps described in [WebUSB] for handling the removal of a device that was
connected to the system.
4.1. USBDevice Behavior
When the open(), close(), selectConfiguration(), claimInterface(), releaseInterface(), selectAlternateInterface(), clearHalt() and reset() methods are invoked on a USBDevice device corresponding to a FakeUSBDevice the UA MUST
behave as though device contains the configurations, interfaces and endpoints
described in the FakeUSBDeviceInit from which device was initialized exist
and can be claimed by the caller.
When close() is invoked on a USBDevice device the UA MUST fire an event named close at the FakeUSBDevice instance corresponding to device, if one exists.
When controlTransferIn(setup, length) is invoked on a USBDevice device corresponding to a FakeUSBDevice the UA MUST,
assuming all other pre-conditions for the operation are satisfied, behave as
though the device responded with a packet containing the bytes [length >> 8, length & 0xFF, setup., truncated to length bytes.request, setup.value >> 8, setup.value & 0xFF, setup.index >> 8, setup.index & 0xFF]
When controlTransferOut(setup, data) is invoked on a USBDevice device corresponding to a FakeUSBDevice the UA MUST, assuming all
other pre-conditions for the operation are satisfied, behave as though the
transfer succeeded in sending data.length bytes.
When transferIn(endpointNumber, length) is invoked on a USBDevice device corresponding to a FakeUSBDevice the UA MUST,
assuming all other pre-conditions for the operation are satisfied, behave as
though the device responded with length bytes of data consisting of the
values 0 through 255 repeated as necessary.
When transferOut(endpointNumber, data) is invoked on a USBDevice device corresponding to a FakeUSBDevice the UA MUST,
assuming all other pre-conditions for the operation are satisfied, behave as
though the transfer succeeded in sending data.length bytes.
When isochronousTransferIn(endpointNumber, packetLengths) is
invoked on a USBDevice device corresponding to a FakeUSBDevice the UA MUST, assuming all other pre-conditions for the operation are satisfied,
behave as though the device responded with packetLengths.length packets, each containing packetLengths[i] bytes of data consisting of the values 0 through 255 repeated as necessary.
When isochronousTransferOut(endpointNumber, data, packetLengths) is invoked on a USBDevice device corresponding to a FakeUSBDevice the UA MUST, assuming all other pre-conditions for the
operation are satisfied, behave as though the transfer succeeded in sending packetLengths.length packets, each containing packetLengths[i] bytes.