OLD | NEW |
| (Empty) |
1 'use strict'; | |
2 | |
3 // This polyfill library implements the WebUSB Test API as specified here: | |
4 // https://wicg.github.io/webusb/test/ | |
5 | |
6 (() => { | |
7 | |
8 // These variables are logically members of the USBTest class but are defined | |
9 // here to hide them from being visible as fields of navigator.usb.test. | |
10 let internal = { | |
11 intialized: false, | |
12 | |
13 deviceManager: null, | |
14 deviceManagerInterceptor: null, | |
15 deviceManagerCrossFrameProxy: null, | |
16 | |
17 chooser: null, | |
18 chooserInterceptor: null, | |
19 chooserCrossFrameProxy: null, | |
20 }; | |
21 | |
22 function fakeDeviceInitToDeviceInfo(guid, init) { | |
23 let deviceInfo = { | |
24 guid: guid + "", | |
25 usbVersionMajor: init.usbVersionMajor, | |
26 usbVersionMinor: init.usbVersionMinor, | |
27 usbVersionSubminor: init.usbVersionSubminor, | |
28 classCode: init.deviceClass, | |
29 subclassCode: init.deviceSubclass, | |
30 protocolCode: init.deviceProtocol, | |
31 vendorId: init.vendorId, | |
32 productId: init.productId, | |
33 deviceVersionMajor: init.deviceVersionMajor, | |
34 deviceVersionMinor: init.deviceVersionMinor, | |
35 deviceVersionSubminor: init.deviceVersionSubminor, | |
36 manufacturerName: init.manufacturerName, | |
37 productName: init.productName, | |
38 serialNumber: init.serialNumber, | |
39 activeConfiguration: init.activeConfigurationValue, | |
40 configurations: [] | |
41 }; | |
42 init.configurations.forEach(config => { | |
43 var configInfo = { | |
44 configurationValue: config.configurationValue, | |
45 configurationName: config.configurationName, | |
46 interfaces: [] | |
47 }; | |
48 config.interfaces.forEach(iface => { | |
49 var interfaceInfo = { | |
50 interfaceNumber: iface.interfaceNumber, | |
51 alternates: [] | |
52 }; | |
53 iface.alternates.forEach(alternate => { | |
54 var alternateInfo = { | |
55 alternateSetting: alternate.alternateSetting, | |
56 classCode: alternate.interfaceClass, | |
57 subclassCode: alternate.interfaceSubclass, | |
58 protocolCode: alternate.interfaceProtocol, | |
59 interfaceName: alternate.interfaceName, | |
60 endpoints: [] | |
61 }; | |
62 alternate.endpoints.forEach(endpoint => { | |
63 var endpointInfo = { | |
64 endpointNumber: endpoint.endpointNumber, | |
65 packetSize: endpoint.packetSize, | |
66 }; | |
67 switch (endpoint.direction) { | |
68 case "in": | |
69 endpointInfo.direction = device.mojom.UsbTransferDirection.INBOUND; | |
70 break; | |
71 case "out": | |
72 endpointInfo.direction = device.mojom.UsbTransferDirection.OUTBOUND; | |
73 break; | |
74 } | |
75 switch (endpoint.type) { | |
76 case "bulk": | |
77 endpointInfo.type = device.mojom.UsbTransferType.BULK; | |
78 break; | |
79 case "interrupt": | |
80 endpointInfo.type = device.mojom.UsbTransferType.INTERRUPT; | |
81 break; | |
82 case "isochronous": | |
83 endpointInfo.type = device.mojom.UsbTransferType.ISOCHRONOUS; | |
84 break; | |
85 } | |
86 alternateInfo.endpoints.push(endpointInfo); | |
87 }); | |
88 interfaceInfo.alternates.push(alternateInfo); | |
89 }); | |
90 configInfo.interfaces.push(interfaceInfo); | |
91 }); | |
92 deviceInfo.configurations.push(configInfo); | |
93 }); | |
94 return deviceInfo; | |
95 } | |
96 | |
97 function convertMojoDeviceFilters(input) { | |
98 let output = []; | |
99 input.forEach(filter => { | |
100 output.push(convertMojoDeviceFilter(filter)); | |
101 }); | |
102 return output; | |
103 } | |
104 | |
105 function convertMojoDeviceFilter(input) { | |
106 let output = {}; | |
107 if (input.hasVendorId) | |
108 output.vendorId = input.vendorId; | |
109 if (input.hasProductId) | |
110 output.productId = input.productId; | |
111 if (input.hasClassCode) | |
112 output.classCode = input.classCode; | |
113 if (input.hasSubclassCode) | |
114 output.subclassCode = input.subclassCode; | |
115 if (input.hasProtocolCode) | |
116 output.protocolCode = input.protocolCode; | |
117 if (input.serialNumber) | |
118 output.serialNumber = input.serialNumber; | |
119 return output; | |
120 } | |
121 | |
122 class FakeDevice { | |
123 constructor(deviceInit) { | |
124 this.info_ = deviceInit; | |
125 this.opened_ = false; | |
126 this.currentConfiguration_ = null; | |
127 this.claimedInterfaces_ = new Map(); | |
128 } | |
129 | |
130 getConfiguration() { | |
131 if (this.currentConfiguration_) { | |
132 return Promise.resolve({ | |
133 value: this.currentConfiguration_.configurationValue }); | |
134 } else { | |
135 return Promise.resolve({ value: 0 }); | |
136 } | |
137 } | |
138 | |
139 open() { | |
140 assert_false(this.opened_); | |
141 this.opened_ = true; | |
142 return Promise.resolve({ error: device.mojom.UsbOpenDeviceError.OK }); | |
143 } | |
144 | |
145 close() { | |
146 assert_true(this.opened_); | |
147 this.opened_ = false; | |
148 return Promise.resolve(); | |
149 } | |
150 | |
151 setConfiguration(value) { | |
152 assert_true(this.opened_); | |
153 | |
154 let selectedConfiguration = this.info_.configurations.find( | |
155 configuration => configuration.configurationValue == value); | |
156 // Blink should never request an invalid configuration. | |
157 assert_not_equals(selectedConfiguration, undefined); | |
158 this.currentConfiguration_ = selectedConfiguration; | |
159 return Promise.resolve({ success: true }); | |
160 } | |
161 | |
162 claimInterface(interfaceNumber) { | |
163 assert_true(this.opened_); | |
164 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
165 assert_false(this.claimedInterfaces_.has(interfaceNumber), | |
166 'interface already claimed'); | |
167 | |
168 // Blink should never request an invalid interface. | |
169 assert_true(this.currentConfiguration_.interfaces.some( | |
170 iface => iface.interfaceNumber == interfaceNumber)); | |
171 this.claimedInterfaces_.set(interfaceNumber, 0); | |
172 return Promise.resolve({ success: true }); | |
173 } | |
174 | |
175 releaseInterface(interfaceNumber) { | |
176 assert_true(this.opened_); | |
177 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
178 assert_true(this.claimedInterfaces_.has(interfaceNumber)); | |
179 this.claimedInterfaces_.delete(interfaceNumber); | |
180 return Promise.resolve({ success: true }); | |
181 } | |
182 | |
183 setInterfaceAlternateSetting(interfaceNumber, alternateSetting) { | |
184 assert_true(this.opened_); | |
185 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
186 assert_true(this.claimedInterfaces_.has(interfaceNumber)); | |
187 | |
188 let iface = this.currentConfiguration_.interfaces.find( | |
189 iface => iface.interfaceNumber == interfaceNumber); | |
190 // Blink should never request an invalid interface or alternate. | |
191 assert_false(iface == undefined); | |
192 assert_true(iface.alternates.some( | |
193 x => x.alternateSetting == alternateSetting)); | |
194 this.claimedInterfaces_.set(interfaceNumber, alternateSetting); | |
195 return Promise.resolve({ success: true }); | |
196 } | |
197 | |
198 reset() { | |
199 assert_true(this.opened_); | |
200 return Promise.resolve({ success: true }); | |
201 } | |
202 | |
203 clearHalt(endpoint) { | |
204 assert_true(this.opened_); | |
205 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
206 // TODO(reillyg): Assert that endpoint is valid. | |
207 return Promise.resolve({ success: true }); | |
208 } | |
209 | |
210 controlTransferIn(params, length, timeout) { | |
211 assert_true(this.opened_); | |
212 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
213 return Promise.resolve({ | |
214 status: device.mojom.UsbTransferStatus.OK, | |
215 data: [length >> 8, length & 0xff, params.request, params.value >> 8, | |
216 params.value & 0xff, params.index >> 8, params.index & 0xff] | |
217 }); | |
218 } | |
219 | |
220 controlTransferOut(params, data, timeout) { | |
221 assert_true(this.opened_); | |
222 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
223 return Promise.resolve({ | |
224 status: device.mojom.UsbTransferStatus.OK, | |
225 bytesWritten: data.byteLength | |
226 }); | |
227 } | |
228 | |
229 genericTransferIn(endpointNumber, length, timeout) { | |
230 assert_true(this.opened_); | |
231 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
232 // TODO(reillyg): Assert that endpoint is valid. | |
233 let data = new Array(length); | |
234 for (let i = 0; i < length; ++i) | |
235 data[i] = i & 0xff; | |
236 return Promise.resolve({ | |
237 status: device.mojom.UsbTransferStatus.OK, | |
238 data: data | |
239 }); | |
240 } | |
241 | |
242 genericTransferOut(endpointNumber, data, timeout) { | |
243 assert_true(this.opened_); | |
244 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
245 // TODO(reillyg): Assert that endpoint is valid. | |
246 return Promise.resolve({ | |
247 status: device.mojom.UsbTransferStatus.OK, | |
248 bytesWritten: data.byteLength | |
249 }); | |
250 } | |
251 | |
252 isochronousTransferIn(endpointNumber, packetLengths, timeout) { | |
253 assert_true(this.opened_); | |
254 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
255 // TODO(reillyg): Assert that endpoint is valid. | |
256 let data = new Array(packetLengths.reduce((a, b) => a + b, 0)); | |
257 let dataOffset = 0; | |
258 let packets = new Array(packetLengths.length); | |
259 for (let i = 0; i < packetLengths.length; ++i) { | |
260 for (let j = 0; j < packetLengths[i]; ++j) | |
261 data[dataOffset++] = j & 0xff; | |
262 packets[i] = { | |
263 length: packetLengths[i], | |
264 transferredLength: packetLengths[i], | |
265 status: device.mojom.UsbTransferStatus.OK | |
266 }; | |
267 } | |
268 return Promise.resolve({ data: data, packets: packets }); | |
269 } | |
270 | |
271 isochronousTransferOut(endpointNumber, data, packetLengths, timeout) { | |
272 assert_true(this.opened_); | |
273 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
274 // TODO(reillyg): Assert that endpoint is valid. | |
275 let packets = new Array(packetLengths.length); | |
276 for (let i = 0; i < packetLengths.length; ++i) { | |
277 packets[i] = { | |
278 length: packetLengths[i], | |
279 transferredLength: packetLengths[i], | |
280 status: device.mojom.UsbTransferStatus.OK | |
281 }; | |
282 } | |
283 return Promise.resolve({ packets: packets }); | |
284 } | |
285 } | |
286 | |
287 class FakeDeviceManager { | |
288 constructor() { | |
289 this.bindingSet_ = new mojo.BindingSet(device.mojom.UsbDeviceManager); | |
290 this.devices_ = new Map(); | |
291 this.devicesByGuid_ = new Map(); | |
292 this.client_ = null; | |
293 this.nextGuid_ = 0; | |
294 } | |
295 | |
296 addBinding(handle) { | |
297 this.bindingSet_.addBinding(this, handle); | |
298 } | |
299 | |
300 addDevice(fakeDevice, info) { | |
301 let device = { | |
302 fakeDevice: fakeDevice, | |
303 guid: (this.nextGuid_++).toString(), | |
304 info: info, | |
305 bindingArray: [] | |
306 }; | |
307 this.devices_.set(fakeDevice, device); | |
308 this.devicesByGuid_.set(device.guid, device); | |
309 if (this.client_) | |
310 this.client_.onDeviceAdded(fakeDeviceInitToDeviceInfo(device.guid, info)); | |
311 } | |
312 | |
313 removeDevice(fakeDevice) { | |
314 let device = this.devices_.get(fakeDevice); | |
315 if (!device) | |
316 throw new Error('Cannot remove unknown device.'); | |
317 | |
318 for (var binding of device.bindingArray) | |
319 binding.close(); | |
320 this.devices_.delete(device.fakeDevice); | |
321 this.devicesByGuid_.delete(device.guid); | |
322 if (this.client_) { | |
323 this.client_.onDeviceRemoved( | |
324 fakeDeviceInitToDeviceInfo(device.guid, device.info)); | |
325 } | |
326 } | |
327 | |
328 removeAllDevices() { | |
329 this.devices_.forEach(device => { | |
330 for (var binding of device.bindingArray) | |
331 binding.close(); | |
332 this.client_.onDeviceRemoved( | |
333 fakeDeviceInitToDeviceInfo(device.guid, device.info)); | |
334 }); | |
335 this.devices_.clear(); | |
336 this.devicesByGuid_.clear(); | |
337 } | |
338 | |
339 getDevices(options) { | |
340 let devices = []; | |
341 this.devices_.forEach(device => { | |
342 devices.push(fakeDeviceInitToDeviceInfo(device.guid, device.info)); | |
343 }); | |
344 return Promise.resolve({ results: devices }); | |
345 } | |
346 | |
347 getDevice(guid, request) { | |
348 let device = this.devicesByGuid_.get(guid); | |
349 if (device) { | |
350 let binding = new mojo.Binding( | |
351 window.device.mojom.UsbDevice, new FakeDevice(device.info), request); | |
352 binding.setConnectionErrorHandler(() => { | |
353 if (device.fakeDevice.onclose) | |
354 device.fakeDevice.onclose(); | |
355 }); | |
356 device.bindingArray.push(binding); | |
357 } else { | |
358 request.close(); | |
359 } | |
360 } | |
361 | |
362 setClient(client) { | |
363 this.client_ = client; | |
364 } | |
365 } | |
366 | |
367 class FakeChooserService { | |
368 constructor() { | |
369 this.bindingSet_ = new mojo.BindingSet(device.mojom.UsbChooserService); | |
370 this.chosenDevice_ = null; | |
371 this.lastFilters_ = null; | |
372 } | |
373 | |
374 addBinding(handle) { | |
375 this.bindingSet_.addBinding(this, handle); | |
376 } | |
377 | |
378 setChosenDevice(fakeDevice) { | |
379 this.chosenDevice_ = fakeDevice; | |
380 } | |
381 | |
382 getPermission(deviceFilters) { | |
383 this.lastFilters_ = convertMojoDeviceFilters(deviceFilters); | |
384 let device = internal.deviceManager.devices_.get(this.chosenDevice_); | |
385 if (device) { | |
386 return Promise.resolve({ | |
387 result: fakeDeviceInitToDeviceInfo(device.guid, device.info) | |
388 }); | |
389 } else { | |
390 return Promise.resolve({ result: null }); | |
391 } | |
392 } | |
393 } | |
394 | |
395 // Unlike FakeDevice this class is exported to callers of USBTest.addFakeDevice. | |
396 class FakeUSBDevice { | |
397 constructor() { | |
398 this.onclose = null; | |
399 } | |
400 | |
401 disconnect() { | |
402 setTimeout(() => internal.deviceManager.removeDevice(this), 0); | |
403 } | |
404 } | |
405 | |
406 // A helper for forwarding MojoHandle instances from one frame to another. | |
407 class CrossFrameHandleProxy { | |
408 constructor(callback) { | |
409 let {handle0, handle1} = Mojo.createMessagePipe(); | |
410 this.sender_ = handle0; | |
411 this.receiver_ = handle1; | |
412 this.receiver_.watch({readable: true}, () => { | |
413 let message = this.receiver_.readMessage(); | |
414 assert_equals(message.buffer.byteLength, 0); | |
415 assert_equals(message.handles.length, 1); | |
416 callback(message.handles[0]); | |
417 }); | |
418 } | |
419 | |
420 forwardHandle(handle) { | |
421 this.sender_.writeMessage(new ArrayBuffer, [handle]); | |
422 } | |
423 } | |
424 | |
425 class USBTest { | |
426 constructor() {} | |
427 | |
428 initialize() { | |
429 if (internal.initialized) | |
430 return Promise.resolve(); | |
431 | |
432 internal.deviceManager = new FakeDeviceManager(); | |
433 internal.deviceManagerInterceptor = | |
434 new MojoInterfaceInterceptor(device.mojom.UsbDeviceManager.name); | |
435 internal.deviceManagerInterceptor.oninterfacerequest = | |
436 e => internal.deviceManager.addBinding(e.handle); | |
437 internal.deviceManagerInterceptor.start(); | |
438 internal.deviceManagerCrossFrameProxy = new CrossFrameHandleProxy( | |
439 handle => internal.deviceManager.addBinding(handle)); | |
440 | |
441 internal.chooser = new FakeChooserService(); | |
442 internal.chooserInterceptor = | |
443 new MojoInterfaceInterceptor(device.mojom.UsbChooserService.name); | |
444 internal.chooserInterceptor.oninterfacerequest = | |
445 e => internal.chooser.addBinding(e.handle); | |
446 internal.chooserInterceptor.start(); | |
447 internal.chooserCrossFrameProxy = new CrossFrameHandleProxy( | |
448 handle => internal.chooser.addBinding(handle)); | |
449 | |
450 internal.initialized = true; | |
451 return Promise.resolve(); | |
452 } | |
453 | |
454 attachToWindow(otherWindow) { | |
455 if (!internal.initialized) | |
456 throw new Error('Call initialize() before attachToWindow().'); | |
457 | |
458 otherWindow.deviceManagerInterceptor = | |
459 new otherWindow.MojoInterfaceInterceptor( | |
460 device.mojom.UsbDeviceManager.name); | |
461 otherWindow.deviceManagerInterceptor.oninterfacerequest = | |
462 e => internal.deviceManagerCrossFrameProxy.forwardHandle(e.handle); | |
463 otherWindow.deviceManagerInterceptor.start(); | |
464 | |
465 otherWindow.chooserInterceptor = | |
466 new otherWindow.MojoInterfaceInterceptor( | |
467 device.mojom.UsbChooserService.name); | |
468 otherWindow.chooserInterceptor.oninterfacerequest = | |
469 e => internal.chooserCrossFrameProxy.forwardHandle(e.handle); | |
470 otherWindow.chooserInterceptor.start(); | |
471 return Promise.resolve(); | |
472 } | |
473 | |
474 addFakeDevice(deviceInit) { | |
475 if (!internal.initialized) | |
476 throw new Error('Call initialize() before addFakeDevice().'); | |
477 | |
478 // |addDevice| and |removeDevice| are called in a setTimeout callback so | |
479 // that tests do not rely on the device being immediately available which | |
480 // may not be true for all implementations of this test API. | |
481 let fakeDevice = new FakeUSBDevice(); | |
482 setTimeout( | |
483 () => internal.deviceManager.addDevice(fakeDevice, deviceInit), 0); | |
484 return fakeDevice; | |
485 } | |
486 | |
487 set chosenDevice(fakeDevice) { | |
488 if (!internal.initialized) | |
489 throw new Error('Call initialize() before setting chosenDevice.'); | |
490 | |
491 internal.chooser.setChosenDevice(fakeDevice); | |
492 } | |
493 | |
494 get lastFilters() { | |
495 if (!internal.initialized) | |
496 throw new Error('Call initialize() before getting lastFilters.'); | |
497 | |
498 return internal.chooser.lastFilters_; | |
499 } | |
500 | |
501 reset() { | |
502 if (!internal.initialized) | |
503 throw new Error('Call initialize() before reset().'); | |
504 | |
505 // Reset the mocks in a setTimeout callback so that tests do not rely on | |
506 // the fact that this polyfill can do this synchronously. | |
507 return new Promise(resolve => { | |
508 setTimeout(() => { | |
509 internal.deviceManager.removeAllDevices(); | |
510 internal.chooser.setChosenDevice(null); | |
511 resolve(); | |
512 }, 0); | |
513 }); | |
514 } | |
515 } | |
516 | |
517 navigator.usb.test = new USBTest(); | |
518 | |
519 })(); | |
OLD | NEW |