OLD | NEW |
| (Empty) |
1 'use strict'; | |
2 | |
3 // This polyfil library implements the following WebIDL: | |
4 // | |
5 // partial interface USB { | |
6 // [SameObject] readonly attribute USBTest test; | |
7 // } | |
8 // | |
9 // interface USBTest { | |
10 // attribute EventHandler ondeviceclose; | |
11 // attribute FakeUSBDevice? chosenDevice; | |
12 // attribute FrozenArray<USBDeviceFilter>? lastFilters; | |
13 // | |
14 // Promise<void> initialize(); | |
15 // Promise<void> attachToWindow(Window window); | |
16 // FakeUSBDevice addFakeDevice(FakeUSBDeviceInit deviceInit); | |
17 // void reset(); | |
18 // }; | |
19 // | |
20 // interface FakeUSBDevice { | |
21 // void disconnect(); | |
22 // }; | |
23 // | |
24 // dictionary FakeUSBDeviceInit { | |
25 // octet usbVersionMajor; | |
26 // octet usbVersionMinor; | |
27 // octet usbVersionSubminor; | |
28 // octet deviceClass; | |
29 // octet deviceSubclass; | |
30 // octet deviceProtocol; | |
31 // unsigned short vendorId; | |
32 // unsigned short productId; | |
33 // octet deviceVersionMajor; | |
34 // octet deviceVersionMinor; | |
35 // octet deviceVersionSubminor; | |
36 // DOMString? manufacturerName; | |
37 // DOMString? productName; | |
38 // DOMString? serialNumber; | |
39 // octet activeConfigurationValue = 0; | |
40 // sequence<FakeUSBConfigurationInit> configurations; | |
41 // }; | |
42 // | |
43 // dictionary FakeUSBConfigurationInit { | |
44 // octet configurationValue; | |
45 // DOMString? configurationName; | |
46 // sequence<FakeUSBInterfaceInit> interfaces; | |
47 // }; | |
48 // | |
49 // dictionary FakeUSBInterfaceInit { | |
50 // octet interfaceNumber; | |
51 // sequence<FakeUSBAlternateInterfaceInit> alternates; | |
52 // }; | |
53 // | |
54 // dictionary FakeUSBAlternateInterfaceInit { | |
55 // octet alternateSetting; | |
56 // octet interfaceClass; | |
57 // octet interfaceSubclass; | |
58 // octet interfaceProtocol; | |
59 // DOMString? interfaceName; | |
60 // sequence<FakeUSBEndpointInit> endpoints; | |
61 // }; | |
62 // | |
63 // dictionary FakeUSBEndpointInit { | |
64 // octet endpointNumber; | |
65 // USBDirection direction; | |
66 // USBEndpointType type; | |
67 // unsigned long packetSize; | |
68 // }; | |
69 | |
70 (() => { | |
71 | |
72 // The global mojo object contains the Mojo JS binding modules loaded during | |
73 // initialization. | |
74 let mojo = null; | |
75 | |
76 // These variables are logically members of the USBTest class but are defined | |
77 // here to hide them from being visible as fields of navigator.usb.test. | |
78 let g_initializePromise = null; | |
79 let g_chooserService = null; | |
80 let g_deviceManager = null; | |
81 let g_closeListener = null; | |
82 let g_nextGuid = 0; | |
83 | |
84 function fakeDeviceInitToDeviceInfo(guid, init) { | |
85 let deviceInfo = { | |
86 guid: guid + "", | |
87 usb_version_major: init.usbVersionMajor, | |
88 usb_version_minor: init.usbVersionMinor, | |
89 usb_version_subminor: init.usbVersionSubminor, | |
90 class_code: init.deviceClass, | |
91 subclass_code: init.deviceSubclass, | |
92 protocol_code: init.deviceProtocol, | |
93 vendor_id: init.vendorId, | |
94 product_id: init.productId, | |
95 device_version_major: init.deviceVersionMajor, | |
96 device_version_minor: init.deviceVersionMinor, | |
97 device_version_subminor: init.deviceVersionSubminor, | |
98 manufacturer_name: init.manufacturerName, | |
99 product_name: init.productName, | |
100 serial_number: init.serialNumber, | |
101 active_configuration: init.activeConfigurationValue, | |
102 configurations: [] | |
103 }; | |
104 init.configurations.forEach(config => { | |
105 var configInfo = { | |
106 configuration_value: config.configurationValue, | |
107 configuration_name: config.configurationName, | |
108 interfaces: [] | |
109 }; | |
110 config.interfaces.forEach(iface => { | |
111 var interfaceInfo = { | |
112 interface_number: iface.interfaceNumber, | |
113 alternates: [] | |
114 }; | |
115 iface.alternates.forEach(alternate => { | |
116 var alternateInfo = { | |
117 alternate_setting: alternate.alternateSetting, | |
118 class_code: alternate.interfaceClass, | |
119 subclass_code: alternate.interfaceSubclass, | |
120 protocol_code: alternate.interfaceProtocol, | |
121 interface_name: alternate.interfaceName, | |
122 endpoints: [] | |
123 }; | |
124 alternate.endpoints.forEach(endpoint => { | |
125 var endpointInfo = { | |
126 endpoint_number: endpoint.endpointNumber, | |
127 packet_size: endpoint.packetSize, | |
128 }; | |
129 switch (endpoint.direction) { | |
130 case "in": | |
131 endpointInfo.direction = mojo.device.TransferDirection.INBOUND; | |
132 break; | |
133 case "out": | |
134 endpointInfo.direction = mojo.device.TransferDirection.OUTBOUND; | |
135 break; | |
136 } | |
137 switch (endpoint.type) { | |
138 case "bulk": | |
139 endpointInfo.type = mojo.device.EndpointType.BULK; | |
140 break; | |
141 case "interrupt": | |
142 endpointInfo.type = mojo.device.EndpointType.INTERRUPT; | |
143 break; | |
144 case "isochronous": | |
145 endpointInfo.type = mojo.device.EndpointType.ISOCHRONOUS; | |
146 break; | |
147 } | |
148 alternateInfo.endpoints.push(endpointInfo); | |
149 }); | |
150 interfaceInfo.alternates.push(alternateInfo); | |
151 }); | |
152 configInfo.interfaces.push(interfaceInfo); | |
153 }); | |
154 deviceInfo.configurations.push(configInfo); | |
155 }); | |
156 return deviceInfo; | |
157 } | |
158 | |
159 function convertMojoDeviceFilters(input) { | |
160 let output = []; | |
161 input.forEach(filter => { | |
162 output.push(convertMojoDeviceFilter(filter)); | |
163 }); | |
164 return output; | |
165 } | |
166 | |
167 function convertMojoDeviceFilter(input) { | |
168 let output = {}; | |
169 if (input.has_vendor_id) | |
170 output.vendorId = input.vendor_id; | |
171 if (input.has_product_id) | |
172 output.productId = input.product_id; | |
173 if (input.has_class_code) | |
174 output.classCode = input.class_code; | |
175 if (input.has_subclass_code) | |
176 output.subclassCode = input.subclass_code; | |
177 if (input.has_protocol_code) | |
178 output.protocolCode = input.protocol_code; | |
179 if (input.serial_number) | |
180 output.serialNumber = input.serial_number; | |
181 return output; | |
182 } | |
183 | |
184 class FakeDevice { | |
185 constructor(deviceInit) { | |
186 this.info_ = deviceInit; | |
187 this.opened_ = false; | |
188 this.currentConfiguration_ = null; | |
189 this.claimedInterfaces_ = new Map(); | |
190 } | |
191 | |
192 getConfiguration() { | |
193 if (this.currentConfiguration_) { | |
194 return Promise.resolve({ | |
195 value: this.currentConfiguration_.configuration_value }); | |
196 } else { | |
197 return Promise.resolve({ value: 0 }); | |
198 } | |
199 } | |
200 | |
201 open() { | |
202 assert_false(this.opened_); | |
203 this.opened_ = true; | |
204 return Promise.resolve({ error: mojo.device.OpenDeviceError.OK }); | |
205 } | |
206 | |
207 close() { | |
208 assert_true(this.opened_); | |
209 this.opened_ = false; | |
210 return Promise.resolve(); | |
211 } | |
212 | |
213 setConfiguration(value) { | |
214 assert_true(this.opened_); | |
215 | |
216 let selected_configuration = this.info_.configurations.find( | |
217 configuration => configuration.configurationValue == value); | |
218 // Blink should never request an invalid configuration. | |
219 assert_false(selected_configuration == undefined); | |
220 this.currentConfiguration_ = selected_configuration; | |
221 return Promise.resolve({ success: true }); | |
222 } | |
223 | |
224 claimInterface(interfaceNumber) { | |
225 assert_true(this.opened_); | |
226 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
227 assert_false(this.claimedInterfaces_.has(interfaceNumber), | |
228 'interface already claimed'); | |
229 | |
230 // Blink should never request an invalid interface. | |
231 assert_true(this.currentConfiguration_.interfaces.some( | |
232 iface => iface.interfaceNumber == interfaceNumber)); | |
233 this.claimedInterfaces_.set(interfaceNumber, 0); | |
234 return Promise.resolve({ success: true }); | |
235 } | |
236 | |
237 releaseInterface(interfaceNumber) { | |
238 assert_true(this.opened_); | |
239 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
240 assert_true(this.claimedInterfaces_.has(interfaceNumber)); | |
241 this.claimedInterfaces_.delete(interfaceNumber); | |
242 return Promise.resolve({ success: true }); | |
243 } | |
244 | |
245 setInterfaceAlternateSetting(interfaceNumber, alternateSetting) { | |
246 assert_true(this.opened_); | |
247 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
248 assert_true(this.claimedInterfaces_.has(interfaceNumber)); | |
249 | |
250 let iface = this.currentConfiguration_.interfaces.find( | |
251 iface => iface.interfaceNumber == interfaceNumber); | |
252 // Blink should never request an invalid interface or alternate. | |
253 assert_false(iface == undefined); | |
254 assert_true(iface.alternates.some( | |
255 x => x.alternateSetting == alternateSetting)); | |
256 this.claimedInterfaces_.set(interfaceNumber, alternateSetting); | |
257 return Promise.resolve({ success: true }); | |
258 } | |
259 | |
260 reset() { | |
261 assert_true(this.opened_); | |
262 return Promise.resolve({ success: true }); | |
263 } | |
264 | |
265 clearHalt(endpoint) { | |
266 assert_true(this.opened_); | |
267 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
268 // TODO(reillyg): Assert that endpoint is valid. | |
269 return Promise.resolve({ success: true }); | |
270 } | |
271 | |
272 controlTransferIn(params, length, timeout) { | |
273 assert_true(this.opened_); | |
274 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
275 return Promise.resolve({ | |
276 status: mojo.device.TransferStatus.OK, | |
277 data: [length >> 8, length & 0xff, params.request, params.value >> 8, | |
278 params.value & 0xff, params.index >> 8, params.index & 0xff] | |
279 }); | |
280 } | |
281 | |
282 controlTransferOut(params, data, timeout) { | |
283 assert_true(this.opened_); | |
284 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
285 return Promise.resolve({ | |
286 status: mojo.device.TransferStatus.OK, | |
287 bytesWritten: data.byteLength | |
288 }); | |
289 } | |
290 | |
291 genericTransferIn(endpointNumber, length, timeout) { | |
292 assert_true(this.opened_); | |
293 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
294 // TODO(reillyg): Assert that endpoint is valid. | |
295 let data = new Array(length); | |
296 for (let i = 0; i < length; ++i) | |
297 data[i] = i & 0xff; | |
298 return Promise.resolve({ | |
299 status: mojo.device.TransferStatus.OK, | |
300 data: data | |
301 }); | |
302 } | |
303 | |
304 genericTransferOut(endpointNumber, data, timeout) { | |
305 assert_true(this.opened_); | |
306 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
307 // TODO(reillyg): Assert that endpoint is valid. | |
308 return Promise.resolve({ | |
309 status: mojo.device.TransferStatus.OK, | |
310 bytesWritten: data.byteLength | |
311 }); | |
312 } | |
313 | |
314 isochronousTransferIn(endpointNumber, packetLengths, timeout) { | |
315 assert_true(this.opened_); | |
316 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
317 // TODO(reillyg): Assert that endpoint is valid. | |
318 let data = new Array(packetLengths.reduce((a, b) => a + b, 0)); | |
319 let dataOffset = 0; | |
320 let packets = new Array(packetLengths.length); | |
321 for (let i = 0; i < packetLengths.length; ++i) { | |
322 for (let j = 0; j < packetLengths[i]; ++j) | |
323 data[dataOffset++] = j & 0xff; | |
324 packets[i] = { | |
325 length: packetLengths[i], | |
326 transferred_length: packetLengths[i], | |
327 status: mojo.device.TransferStatus.OK | |
328 }; | |
329 } | |
330 return Promise.resolve({ data: data, packets: packets }); | |
331 } | |
332 | |
333 isochronousTransferOut(endpointNumber, data, packetLengths, timeout) { | |
334 assert_true(this.opened_); | |
335 assert_false(this.currentConfiguration_ == null, 'device configured'); | |
336 // TODO(reillyg): Assert that endpoint is valid. | |
337 let packets = new Array(packetLengths.length); | |
338 for (let i = 0; i < packetLengths.length; ++i) { | |
339 packets[i] = { | |
340 length: packetLengths[i], | |
341 transferred_length: packetLengths[i], | |
342 status: mojo.device.TransferStatus.OK | |
343 }; | |
344 } | |
345 return Promise.resolve({ packets: packets }); | |
346 } | |
347 } | |
348 | |
349 class FakeDeviceManager { | |
350 constructor() { | |
351 this.bindingSet_ = | |
352 new mojo.bindings.BindingSet(mojo.deviceManager.DeviceManager); | |
353 this.devices_ = new Map(); | |
354 this.devicesByGuid_ = new Map(); | |
355 this.client_ = null; | |
356 this.nextGuid_ = 0; | |
357 } | |
358 | |
359 addBinding(handle) { | |
360 this.bindingSet_.addBinding(this, handle); | |
361 } | |
362 | |
363 addDevice(fakeDevice, info) { | |
364 let device = { | |
365 fakeDevice: fakeDevice, | |
366 guid: (this.nextGuid_++).toString(), | |
367 info: info, | |
368 bindingArray: [] | |
369 }; | |
370 this.devices_.set(fakeDevice, device); | |
371 this.devicesByGuid_.set(device.guid, device); | |
372 if (this.client_) | |
373 this.client_.onDeviceAdded(fakeDeviceInitToDeviceInfo(device.guid, info)); | |
374 } | |
375 | |
376 removeDevice(fakeDevice) { | |
377 let device = this.devices_.get(fakeDevice); | |
378 if (!device) | |
379 throw new Error('Cannot remove unknown device.'); | |
380 | |
381 for (var binding of device.bindingArray) | |
382 binding.close(); | |
383 this.devices_.delete(device.fakeDevice); | |
384 this.devicesByGuid_.delete(device.guid); | |
385 if (this.client_) { | |
386 this.client_.onDeviceRemoved( | |
387 fakeDeviceInitToDeviceInfo(device.guid, device.info)); | |
388 } | |
389 } | |
390 | |
391 removeAllDevices() { | |
392 this.devices_.forEach(device => { | |
393 for (var binding of device.bindingArray) | |
394 binding.close(); | |
395 this.client_.onDeviceRemoved( | |
396 fakeDeviceInitToDeviceInfo(device.guid, device.info)); | |
397 }); | |
398 this.devices_.clear(); | |
399 this.devicesByGuid_.clear(); | |
400 } | |
401 | |
402 getDevices(options) { | |
403 let devices = []; | |
404 this.devices_.forEach(device => { | |
405 devices.push(fakeDeviceInitToDeviceInfo(device.guid, device.info)); | |
406 }); | |
407 return Promise.resolve({ results: devices }); | |
408 } | |
409 | |
410 getDevice(guid, request) { | |
411 let device = this.devicesByGuid_.get(guid); | |
412 if (device) { | |
413 let binding = new mojo.bindings.Binding( | |
414 mojo.device.Device, new FakeDevice(device.info), request); | |
415 binding.setConnectionErrorHandler(() => { | |
416 if (g_closeListener) | |
417 g_closeListener(device.fakeDevice); | |
418 }); | |
419 device.bindingArray.push(binding); | |
420 } else { | |
421 request.close(); | |
422 } | |
423 } | |
424 | |
425 setClient(client) { | |
426 this.client_ = client; | |
427 } | |
428 } | |
429 | |
430 class FakeChooserService { | |
431 constructor() { | |
432 this.bindingSet_ = new mojo.bindings.BindingSet( | |
433 mojo.chooserService.ChooserService); | |
434 this.chosenDevice_ = null; | |
435 this.lastFilters_ = null; | |
436 } | |
437 | |
438 addBinding(handle) { | |
439 this.bindingSet_.addBinding(this, handle); | |
440 } | |
441 | |
442 setChosenDevice(fakeDevice) { | |
443 this.chosenDevice_ = fakeDevice; | |
444 } | |
445 | |
446 getPermission(deviceFilters) { | |
447 this.lastFilters_ = convertMojoDeviceFilters(deviceFilters); | |
448 let device = g_deviceManager.devices_.get(this.chosenDevice_); | |
449 if (device) { | |
450 return Promise.resolve({ | |
451 result: fakeDeviceInitToDeviceInfo(device.guid, device.info) | |
452 }); | |
453 } else { | |
454 return Promise.resolve({ result: null }); | |
455 } | |
456 } | |
457 } | |
458 | |
459 // Unlike FakeDevice this class is exported to callers of USBTest.addFakeDevice. | |
460 class FakeUSBDevice { | |
461 disconnect() { | |
462 setTimeout(() => g_deviceManager.removeDevice(this), 0); | |
463 } | |
464 } | |
465 | |
466 class USBTest { | |
467 constructor() {} | |
468 | |
469 initialize() { | |
470 if (!g_initializePromise) { | |
471 g_initializePromise = new Promise(resolve => { | |
472 window.define = gin.define; // Mojo modules expect this. | |
473 | |
474 gin.define('WebUSB Test Mocks', [ | |
475 'content/public/renderer/frame_interfaces', | |
476 'device/usb/public/interfaces/chooser_service.mojom', | |
477 'device/usb/public/interfaces/device_manager.mojom', | |
478 'device/usb/public/interfaces/device.mojom', | |
479 'mojo/public/js/bindings', | |
480 'mojo/public/js/core', | |
481 'mojo/public/js/router', | |
482 'mojo/public/js/support', | |
483 ], (frameInterfaces, chooserService, deviceManager, device, | |
484 bindings, core, router, support) => { | |
485 delete window.define; // Clean up. | |
486 | |
487 mojo = { | |
488 frameInterfaces: frameInterfaces, | |
489 chooserService: chooserService, | |
490 deviceManager: deviceManager, | |
491 device: device, | |
492 bindings: bindings, | |
493 core: core, | |
494 router: router, | |
495 support: support | |
496 }; | |
497 | |
498 g_deviceManager = new FakeDeviceManager(); | |
499 mojo.frameInterfaces.addInterfaceOverrideForTesting( | |
500 mojo.deviceManager.DeviceManager.name, | |
501 handle => g_deviceManager.addBinding(handle)); | |
502 | |
503 g_chooserService = new FakeChooserService(); | |
504 mojo.frameInterfaces.addInterfaceOverrideForTesting( | |
505 mojo.chooserService.ChooserService.name, | |
506 handle => g_chooserService.addBinding(handle)); | |
507 | |
508 addEventListener('unload', () => { | |
509 mojo.frameInterfaces.clearInterfaceOverridesForTesting(); | |
510 }); | |
511 | |
512 resolve(); | |
513 }); | |
514 }); | |
515 } | |
516 | |
517 return g_initializePromise; | |
518 } | |
519 | |
520 attachToWindow(otherWindow) { | |
521 if (!g_deviceManager || !g_chooserService) | |
522 throw new Error('Call initialize() before attachToWindow().'); | |
523 | |
524 return new Promise(resolve => { | |
525 otherWindow.gin.define( | |
526 'WebUSB Test Frame Attach', [ | |
527 'content/public/renderer/frame_interfaces' | |
528 ], frameInterfaces => { | |
529 frameInterfaces.addInterfaceOverrideForTesting( | |
530 mojo.deviceManager.DeviceManager.name, | |
531 handle => g_deviceManager.addBinding(handle)); | |
532 frameInterfaces.addInterfaceOverrideForTesting( | |
533 mojo.chooserService.ChooserService.name, | |
534 handle => g_chooserService.addBinding(handle)); | |
535 resolve(); | |
536 }); | |
537 }); | |
538 } | |
539 | |
540 addFakeDevice(deviceInit) { | |
541 if (!g_deviceManager) | |
542 throw new Error('Call initialize() before addFakeDevice().'); | |
543 | |
544 // |addDevice| and |removeDevice| are called in a setTimeout callback so | |
545 // that tests do not rely on the device being immediately available which | |
546 // may not be true for all implementations of this test API. | |
547 let fakeDevice = new FakeUSBDevice(); | |
548 setTimeout(() => g_deviceManager.addDevice(fakeDevice, deviceInit), 0); | |
549 return fakeDevice; | |
550 } | |
551 | |
552 set ondeviceclose(func) { | |
553 g_closeListener = func; | |
554 } | |
555 | |
556 set chosenDevice(fakeDevice) { | |
557 if (!g_chooserService) | |
558 throw new Error('Call initialize() before setting chosenDevice.'); | |
559 | |
560 g_chooserService.setChosenDevice(fakeDevice); | |
561 } | |
562 | |
563 get lastFilters() { | |
564 if (!g_chooserService) | |
565 throw new Error('Call initialize() before getting lastFilters.'); | |
566 | |
567 return g_chooserService.lastFilters_; | |
568 } | |
569 | |
570 reset() { | |
571 if (!g_deviceManager || !g_chooserService) | |
572 throw new Error('Call initialize() before reset().'); | |
573 | |
574 g_deviceManager.removeAllDevices(); | |
575 g_chooserService.setChosenDevice(null); | |
576 g_closeListener = null; | |
577 } | |
578 } | |
579 | |
580 navigator.usb.test = new USBTest(); | |
581 | |
582 })(); | |
OLD | NEW |