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 DOMString? chosenDevice; | |
12 // attribute FrozenArray<USBDeviceFilter>? lastFilters; | |
13 // | |
14 // Promise<void> initialize(); | |
15 // Promise<void> attachToWindow(Window window); | |
16 // DOMString addFakeDevice(FakeUSBDeviceInit deviceInit); | |
17 // void removeFakeDevice(DOMString); | |
18 // void reset(); | |
19 // }; | |
20 // | |
21 // dictionary FakeUSBDeviceInit { | |
22 // octet usbVersionMajor; | |
23 // octet usbVersionMinor; | |
24 // octet usbVersionSubminor; | |
25 // octet deviceClass; | |
26 // octet deviceSubclass; | |
27 // octet deviceProtocol; | |
28 // unsigned short vendorId; | |
29 // unsigned short productId; | |
30 // octet deviceVersionMajor; | |
31 // octet deviceVersionMinor; | |
32 // octet deviceVersionSubminor; | |
33 // DOMString? manufacturerName; | |
34 // DOMString? productName; | |
35 // DOMString? serialNumber; | |
36 // octet activeConfigurationValue = 0; | |
37 // sequence<FakeUSBConfigurationInit> configurations; | |
38 // }; | |
39 // | |
40 // dictionary FakeUSBConfigurationInit { | |
41 // octet configurationValue; | |
42 // DOMString? configurationName; | |
43 // sequence<FakeUSBInterfaceInit> interfaces; | |
44 // }; | |
45 // | |
46 // dictionary FakeUSBInterfaceInit { | |
47 // octet interfaceNumber; | |
48 // sequence<FakeUSBAlternateInterfaceInit> alternates; | |
49 // }; | |
50 // | |
51 // dictionary FakeUSBAlternateInterfaceInit { | |
52 // octet alternateSetting; | |
53 // octet interfaceClass; | |
54 // octet interfaceSubclass; | |
55 // octet interfaceProtocol; | |
56 // DOMString? interfaceName; | |
57 // sequence<FakeUSBEndpointInit> endpoints; | |
58 // }; | |
59 // | |
60 // dictionary FakeUSBEndpointInit { | |
61 // octet endpointNumber; | |
62 // USBDirection direction; | |
63 // USBEndpointType type; | |
64 // unsigned long packetSize; | |
65 // }; | |
66 | |
67 (() => { | |
68 | |
69 // The global mojo object contains the Mojo JS binding modules loaded during | |
70 // initialization. | |
71 let mojo = null; | |
72 | |
73 // These variables are logically members of the USBTest class but are defined | |
74 // here to hide them from being visible as fields of navigator.usb.test. | |
75 let g_initializePromise = null; | |
76 let g_chooserService = null; | |
77 let g_deviceManager = null; | |
78 let g_closeListener = null; | |
79 | |
80 function fakeDeviceInitToDeviceInfo(guid, init) { | |
81 let deviceInfo = { | |
82 guid: guid + "", | |
83 usb_version_major: init.usbVersionMajor, | |
84 usb_version_minor: init.usbVersionMinor, | |
85 usb_version_subminor: init.usbVersionSubminor, | |
86 class_code: init.deviceClass, | |
87 subclass_code: init.deviceSubclass, | |
88 protocol_code: init.deviceProtocol, | |
89 vendor_id: init.vendorId, | |
90 product_id: init.productId, | |
91 device_version_major: init.deviceVersionMajor, | |
92 device_version_minor: init.deviceVersionMinor, | |
93 device_version_subminor: init.deviceVersionSubminor, | |
94 manufacturer_name: init.manufacturerName, | |
95 product_name: init.productName, | |
96 serial_number: init.serialNumber, | |
97 active_configuration: init.activeConfigurationValue, | |
98 configurations: [] | |
99 }; | |
100 init.configurations.forEach(config => { | |
101 var configInfo = { | |
102 configuration_value: config.configurationValue, | |
103 configuration_name: config.configurationName, | |
104 interfaces: [] | |
105 }; | |
106 config.interfaces.forEach(iface => { | |
107 var interfaceInfo = { | |
108 interface_number: iface.interfaceNumber, | |
109 alternates: [] | |
110 }; | |
111 iface.alternates.forEach(alternate => { | |
112 var alternateInfo = { | |
113 alternate_setting: alternate.alternateSetting, | |
114 class_code: alternate.interfaceClass, | |
115 subclass_code: alternate.interfaceSubclass, | |
116 protocol_code: alternate.interfaceProtocol, | |
117 interface_name: alternate.interfaceName, | |
118 endpoints: [] | |
119 }; | |
120 alternate.endpoints.forEach(endpoint => { | |
121 var endpointInfo = { | |
122 endpoint_number: endpoint.endpointNumber, | |
123 packet_size: endpoint.packetSize, | |
124 }; | |
125 switch (endpoint.direction) { | |
126 case "in": | |
127 endpointInfo.direction = mojo.device.TransferDirection.INBOUND; | |
128 break; | |
129 case "out": | |
130 endpointInfo.direction = mojo.device.TransferDirection.OUTBOUND; | |
131 break; | |
132 } | |
133 switch (endpoint.type) { | |
134 case "bulk": | |
135 endpointInfo.type = mojo.device.EndpointType.BULK; | |
136 break; | |
137 case "interrupt": | |
138 endpointInfo.type = mojo.device.EndpointType.INTERRUPT; | |
139 break; | |
140 case "isochronous": | |
141 endpointInfo.type = mojo.device.EndpointType.ISOCHRONOUS; | |
142 break; | |
143 } | |
144 alternateInfo.endpoints.push(endpointInfo); | |
145 }); | |
146 interfaceInfo.alternates.push(alternateInfo); | |
147 }); | |
148 configInfo.interfaces.push(interfaceInfo); | |
149 }); | |
150 deviceInfo.configurations.push(configInfo); | |
151 }); | |
152 return deviceInfo; | |
153 } | |
154 | |
155 function convertMojoDeviceFilters(input) { | |
156 let output = []; | |
157 input.forEach(filter => { | |
158 output.push(convertMojoDeviceFilter(filter)); | |
159 }); | |
160 return output; | |
161 } | |
162 | |
163 function convertMojoDeviceFilter(input) { | |
164 let output = {}; | |
165 if (input.has_vendor_id) | |
166 output.vendorId = input.vendor_id; | |
167 if (input.has_product_id) | |
168 output.productId = input.product_id; | |
169 if (input.has_class_code) | |
170 output.classCode = input.class_code; | |
171 if (input.has_subclass_code) | |
172 output.subclassCode = input.subclass_code; | |
173 if (input.has_protocol_code) | |
174 output.protocolCode = input.protocol_code; | |
175 if (input.serial_number) | |
176 output.serialNumber = input.serial_number; | |
177 return output; | |
178 } | |
179 | |
180 class FakeDevice { | |
181 constructor(deviceInit) { | |
182 this.info_ = deviceInit; | |
183 this.opened_ = false; | |
184 this.currentConfiguration_ = null; | |
185 this.claimedInterfaces_ = new Map(); | |
186 } | |
187 | |
188 getDeviceInfo() { | |
189 return Promise.resolve({ info: this.info_ }); | |
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.client_ = null; | |
355 } | |
356 | |
357 addBinding(handle) { | |
358 this.bindingSet_.addBinding(this, handle); | |
359 } | |
360 | |
361 addDevice(info) { | |
362 let device = { | |
363 guid: this.nextGuid_++ + "", | |
364 info: info, | |
365 bindingArray: [] | |
366 }; | |
367 this.devices_.set(device.guid, device); | |
368 if (this.client_) | |
369 this.client_.onDeviceAdded(fakeDeviceInitToDeviceInfo(device.guid, info)); | |
370 return device.guid; | |
371 } | |
372 | |
373 removeDevice(guid) { | |
374 let device = this.devices_.get(guid); | |
375 for (var binding of device.bindingArray) | |
376 binding.close(); | |
377 this.devices_.delete(guid); | |
378 if (this.client_) { | |
379 this.client_.onDeviceRemoved( | |
380 fakeDeviceInitToDeviceInfo(guid, device.info)); | |
381 } | |
382 } | |
383 | |
384 removeAllDevices() { | |
385 this.devices_.forEach(device => { | |
386 for (var binding of device.bindingArray) | |
387 binding.close(); | |
388 this.client_.onDeviceRemoved( | |
389 fakeDeviceInitToDeviceInfo(device.guid, device.info)); | |
390 }); | |
391 this.devices_.clear(); | |
392 } | |
393 | |
394 getDevices(options) { | |
395 let devices = []; | |
396 this.devices_.forEach(device => { | |
397 devices.push(fakeDeviceInitToDeviceInfo(device.guid, device.info)); | |
398 }); | |
399 return Promise.resolve({ results: devices }); | |
400 } | |
401 | |
402 getDevice(guid, request) { | |
403 let device = this.devices_.get(guid); | |
404 if (device) { | |
405 let binding = new mojo.bindings.Binding( | |
406 mojo.device.Device, new FakeDevice(device.info), request); | |
407 binding.setConnectionErrorHandler(() => { | |
408 if (g_closeListener) | |
409 g_closeListener(guid); | |
410 }); | |
411 device.bindingArray.push(binding); | |
412 } else { | |
413 request.close(); | |
414 } | |
415 } | |
416 | |
417 setClient(client) { | |
418 this.client_ = client; | |
419 } | |
420 } | |
421 | |
422 class FakeChooserService { | |
423 constructor() { | |
424 this.bindingSet_ = new mojo.bindings.BindingSet( | |
425 mojo.chooserService.ChooserService); | |
426 this.chosenDevice_ = null; | |
427 this.lastFilters_ = null; | |
428 } | |
429 | |
430 addBinding(handle) { | |
431 this.bindingSet_.addBinding(this, handle); | |
432 } | |
433 | |
434 setChosenDevice(guid) { | |
435 this.chosenDeviceGuid_ = guid; | |
436 } | |
437 | |
438 getPermission(deviceFilters) { | |
439 this.lastFilters_ = convertMojoDeviceFilters(deviceFilters); | |
440 let device = g_deviceManager.devices_.get(this.chosenDeviceGuid_); | |
441 if (device) { | |
442 return Promise.resolve({ | |
443 result: fakeDeviceInitToDeviceInfo(device.guid, device.info) | |
444 }); | |
445 } else { | |
446 return Promise.resolve({ result: null }); | |
447 } | |
448 } | |
449 } | |
450 | |
451 class USBTest { | |
452 constructor() {} | |
453 | |
454 initialize() { | |
455 if (!g_initializePromise) { | |
456 g_initializePromise = new Promise(resolve => { | |
457 window.define = gin.define; // Mojo modules expect this. | |
458 | |
459 gin.define('WebUSB Test Mocks', [ | |
460 'content/public/renderer/frame_interfaces', | |
461 'device/usb/public/interfaces/chooser_service.mojom', | |
462 'device/usb/public/interfaces/device_manager.mojom', | |
463 'device/usb/public/interfaces/device.mojom', | |
464 'mojo/public/js/bindings', | |
465 'mojo/public/js/core', | |
466 'mojo/public/js/router', | |
467 'mojo/public/js/support', | |
468 ], (frameInterfaces, chooserService, deviceManager, device, | |
469 bindings, core, router, support) => { | |
470 delete window.define; // Clean up. | |
471 | |
472 mojo = { | |
473 frameInterfaces: frameInterfaces, | |
474 chooserService: chooserService, | |
475 deviceManager: deviceManager, | |
476 device: device, | |
477 bindings: bindings, | |
478 core: core, | |
479 router: router, | |
480 support: support | |
481 }; | |
482 | |
483 g_deviceManager = new FakeDeviceManager(); | |
484 mojo.frameInterfaces.addInterfaceOverrideForTesting( | |
485 mojo.deviceManager.DeviceManager.name, | |
486 handle => g_deviceManager.addBinding(handle)); | |
487 | |
488 g_chooserService = new FakeChooserService(); | |
489 mojo.frameInterfaces.addInterfaceOverrideForTesting( | |
490 mojo.chooserService.ChooserService.name, | |
491 handle => g_chooserService.addBinding(handle)); | |
492 | |
493 resolve(); | |
494 }); | |
495 }); | |
496 } | |
497 | |
498 return g_initializePromise; | |
499 } | |
500 | |
501 attachToWindow(otherWindow) { | |
502 if (!g_deviceManager || !g_chooserService) | |
503 throw new Error('Call initialize() before attachToWindow().'); | |
504 | |
505 return new Promise(resolve => { | |
506 otherWindow.gin.define( | |
507 'WebUSB Test Frame Attach', [ | |
508 'content/public/renderer/frame_interfaces' | |
509 ], frameInterfaces => { | |
510 frameInterfaces.addInterfaceOverrideForTesting( | |
511 mojo.deviceManager.DeviceManager.name, | |
512 handle => g_deviceManager.addBinding(handle)); | |
513 frameInterfaces.addInterfaceOverrideForTesting( | |
514 mojo.chooserService.ChooserService.name, | |
515 handle => g_chooserService.addBinding(handle)); | |
516 resolve(); | |
517 }); | |
518 }); | |
519 } | |
520 | |
521 addFakeDevice(deviceInit) { | |
522 if (!g_deviceManager) | |
523 throw new Error('Call initialize() before addFakeDevice().'); | |
524 | |
525 return g_deviceManager.addDevice(deviceInit); | |
526 } | |
527 | |
528 removeFakeDevice(guid) { | |
529 if (!g_deviceManager) | |
530 throw new Error('Call initialize() before removeFakeDevice().'); | |
531 | |
532 return g_deviceManager.removeDevice(guid); | |
533 } | |
534 | |
535 set ondeviceclose(func) { | |
536 g_closeListener = func; | |
537 } | |
538 | |
539 set chosenDevice(guid) { | |
540 if (!g_chooserService) | |
541 throw new Error('Call initialize() before setting chosenDevice.'); | |
542 | |
543 g_chooserService.setChosenDevice(guid); | |
544 } | |
545 | |
546 get lastFilters() { | |
547 if (!g_chooserService) | |
548 throw new Error('Call initialize() before getting lastFilters.'); | |
549 | |
550 return g_chooserService.lastFilters_; | |
551 } | |
552 | |
553 reset() { | |
554 if (!g_deviceManager || !g_chooserService) | |
555 throw new Error('Call initialize() before reset().'); | |
556 | |
557 g_deviceManager.removeAllDevices(); | |
558 g_chooserService.setChosenDevice(null); | |
559 g_closeListener = null; | |
560 } | |
561 } | |
562 | |
563 navigator.usb.test = new USBTest(); | |
564 | |
565 })(); | |
OLD | NEW |