fake-hid.js (10471B)
1 import {HidConnectionReceiver, HidDeviceInfo} from '/gen/services/device/public/mojom/hid.mojom.m.js'; 2 import {HidService, HidServiceReceiver} from '/gen/third_party/blink/public/mojom/hid/hid.mojom.m.js'; 3 4 // Fake implementation of device.mojom.HidConnection. HidConnection represents 5 // an open connection to a HID device and can be used to send and receive 6 // reports. 7 class FakeHidConnection { 8 constructor(client) { 9 this.client_ = client; 10 this.receiver_ = new HidConnectionReceiver(this); 11 this.expectedWrites_ = []; 12 this.expectedGetFeatureReports_ = []; 13 this.expectedSendFeatureReports_ = []; 14 } 15 16 bindNewPipeAndPassRemote() { 17 return this.receiver_.$.bindNewPipeAndPassRemote(); 18 } 19 20 // Simulate an input report sent from the device to the host. The connection 21 // client's onInputReport method will be called with the provided |reportId| 22 // and |buffer|. 23 simulateInputReport(reportId, reportData) { 24 if (this.client_) { 25 this.client_.onInputReport(reportId, reportData); 26 } 27 } 28 29 // Specify the result for an expected call to write. If |success| is true the 30 // write will be successful, otherwise it will simulate a failure. The 31 // parameters of the next write call must match |reportId| and |buffer|. 32 queueExpectedWrite(success, reportId, reportData) { 33 this.expectedWrites_.push({ 34 params: {reportId, data: reportData}, 35 result: {success}, 36 }); 37 } 38 39 // Specify the result for an expected call to getFeatureReport. If |success| 40 // is true the operation is successful, otherwise it will simulate a failure. 41 // The parameter of the next getFeatureReport call must match |reportId|. 42 queueExpectedGetFeatureReport(success, reportId, reportData) { 43 this.expectedGetFeatureReports_.push({ 44 params: {reportId}, 45 result: {success, buffer: reportData}, 46 }); 47 } 48 49 // Specify the result for an expected call to sendFeatureReport. If |success| 50 // is true the operation is successful, otherwise it will simulate a failure. 51 // The parameters of the next sendFeatureReport call must match |reportId| and 52 // |buffer|. 53 queueExpectedSendFeatureReport(success, reportId, reportData) { 54 this.expectedSendFeatureReports_.push({ 55 params: {reportId, data: reportData}, 56 result: {success}, 57 }); 58 } 59 60 // Asserts that there are no more expected operations. 61 assertExpectationsMet() { 62 assert_equals(this.expectedWrites_.length, 0); 63 assert_equals(this.expectedGetFeatureReports_.length, 0); 64 assert_equals(this.expectedSendFeatureReports_.length, 0); 65 } 66 67 read() {} 68 69 // Implementation of HidConnection::Write. Causes an assertion failure if 70 // there are no expected write operations, or if the parameters do not match 71 // the expected call. 72 async write(reportId, buffer) { 73 let expectedWrite = this.expectedWrites_.shift(); 74 assert_not_equals(expectedWrite, undefined); 75 assert_equals(reportId, expectedWrite.params.reportId); 76 let actual = new Uint8Array(buffer); 77 compareDataViews( 78 new DataView(actual.buffer, actual.byteOffset), 79 new DataView( 80 expectedWrite.params.data.buffer, 81 expectedWrite.params.data.byteOffset)); 82 return expectedWrite.result; 83 } 84 85 // Implementation of HidConnection::GetFeatureReport. Causes an assertion 86 // failure if there are no expected write operations, or if the parameters do 87 // not match the expected call. 88 async getFeatureReport(reportId) { 89 let expectedGetFeatureReport = this.expectedGetFeatureReports_.shift(); 90 assert_not_equals(expectedGetFeatureReport, undefined); 91 assert_equals(reportId, expectedGetFeatureReport.params.reportId); 92 return expectedGetFeatureReport.result; 93 } 94 95 // Implementation of HidConnection::SendFeatureReport. Causes an assertion 96 // failure if there are no expected write operations, or if the parameters do 97 // not match the expected call. 98 async sendFeatureReport(reportId, buffer) { 99 let expectedSendFeatureReport = this.expectedSendFeatureReports_.shift(); 100 assert_not_equals(expectedSendFeatureReport, undefined); 101 assert_equals(reportId, expectedSendFeatureReport.params.reportId); 102 let actual = new Uint8Array(buffer); 103 compareDataViews( 104 new DataView(actual.buffer, actual.byteOffset), 105 new DataView( 106 expectedSendFeatureReport.params.data.buffer, 107 expectedSendFeatureReport.params.data.byteOffset)); 108 return expectedSendFeatureReport.result; 109 } 110 } 111 112 113 // A fake implementation of the HidService mojo interface. HidService manages 114 // HID device access for clients in the render process. Typically, when a client 115 // requests access to a HID device a chooser dialog is shown with a list of 116 // available HID devices. Selecting a device from the chooser also grants 117 // permission for the client to access that device. 118 // 119 // The fake implementation allows tests to simulate connected devices. It also 120 // skips the chooser dialog and instead allows tests to specify which device 121 // should be selected. All devices are treated as if the user had already 122 // granted permission. It is possible to revoke permission with forget() later. 123 class FakeHidService { 124 constructor() { 125 this.interceptor_ = new MojoInterfaceInterceptor(HidService.$interfaceName); 126 this.interceptor_.oninterfacerequest = e => this.bind(e.handle); 127 this.receiver_ = new HidServiceReceiver(this); 128 this.nextGuidValue_ = 0; 129 this.simulateConnectFailure_ = false; 130 this.reset(); 131 } 132 133 start() { 134 this.interceptor_.start(); 135 } 136 137 stop() { 138 this.interceptor_.stop(); 139 } 140 141 reset() { 142 this.devices_ = new Map(); 143 this.allowedDevices_ = new Map(); 144 this.fakeConnections_ = new Map(); 145 this.selectedDevices_ = []; 146 } 147 148 // Creates and returns a HidDeviceInfo with the specified device IDs. 149 makeDevice(vendorId, productId) { 150 let guidValue = ++this.nextGuidValue_; 151 let info = new HidDeviceInfo(); 152 info.guid = 'guid-' + guidValue.toString(); 153 info.physicalDeviceId = 'physical-device-id-' + guidValue.toString(); 154 info.vendorId = vendorId; 155 info.productId = productId; 156 info.productName = 'product name'; 157 info.serialNumber = '0'; 158 info.reportDescriptor = new Uint8Array(); 159 info.collections = []; 160 info.deviceNode = 'device node'; 161 return info; 162 } 163 164 // Simulates a connected device the client has already been granted permission 165 // to. Returns the key used to store the device in the map. The key is either 166 // the physical device ID, or the device GUID if it has no physical device ID. 167 addDevice(deviceInfo, grantPermission = true) { 168 let key = deviceInfo.physicalDeviceId; 169 if (key.length === 0) 170 key = deviceInfo.guid; 171 172 let devices = this.devices_.get(key) || []; 173 devices.push(deviceInfo); 174 this.devices_.set(key, devices); 175 176 if (grantPermission) { 177 let allowedDevices = this.allowedDevices_.get(key) || []; 178 allowedDevices.push(deviceInfo); 179 this.allowedDevices_.set(key, allowedDevices); 180 } 181 182 if (this.client_) 183 this.client_.deviceAdded(deviceInfo); 184 return key; 185 } 186 187 // Simulates disconnecting a connected device. 188 removeDevice(key) { 189 let devices = this.devices_.get(key); 190 this.devices_.delete(key); 191 if (this.client_ && devices) { 192 devices.forEach(deviceInfo => { 193 this.client_.deviceRemoved(deviceInfo); 194 }); 195 } 196 } 197 198 // Simulates updating the device information for a connected device. 199 changeDevice(deviceInfo) { 200 let key = deviceInfo.physicalDeviceId; 201 if (key.length === 0) 202 key = deviceInfo.guid; 203 204 let devices = this.devices_.get(key) || []; 205 let i = devices.length; 206 while (i--) { 207 if (devices[i].guid == deviceInfo.guid) 208 devices.splice(i, 1); 209 } 210 devices.push(deviceInfo); 211 this.devices_.set(key, devices); 212 213 let allowedDevices = this.allowedDevices_.get(key) || []; 214 let j = allowedDevices.length; 215 while (j--) { 216 if (allowedDevices[j].guid == deviceInfo.guid) 217 allowedDevices.splice(j, 1); 218 } 219 allowedDevices.push(deviceInfo); 220 this.allowedDevices_.set(key, allowedDevices); 221 222 if (this.client_) 223 this.client_.deviceChanged(deviceInfo); 224 return key; 225 } 226 227 // Sets a flag that causes the next call to connect() to fail. 228 simulateConnectFailure() { 229 this.simulateConnectFailure_ = true; 230 } 231 232 // Sets the key of the device that will be returned as the selected item the 233 // next time requestDevice is called. The device with this key must have been 234 // previously added with addDevice. 235 setSelectedDevice(key) { 236 this.selectedDevices_ = this.devices_.get(key); 237 } 238 239 // Returns the fake HidConnection object for this device, if there is one. A 240 // connection is created once the device is opened. 241 getFakeConnection(guid) { 242 return this.fakeConnections_.get(guid); 243 } 244 245 bind(handle) { 246 this.receiver_.$.bindHandle(handle); 247 } 248 249 registerClient(client) { 250 this.client_ = client; 251 } 252 253 // Returns an array of connected devices the client has already been granted 254 // permission to access. 255 async getDevices() { 256 let devices = []; 257 this.allowedDevices_.forEach((value) => { 258 devices = devices.concat(value); 259 }); 260 return {devices}; 261 } 262 263 // Simulates a device chooser prompt, returning |selectedDevices_| as the 264 // simulated selection. |options| is ignored. 265 async requestDevice(options) { 266 return {devices: this.selectedDevices_}; 267 } 268 269 // Returns a fake connection to the device with the specified GUID. If 270 // |connectionClient| is not null, its onInputReport method will be called 271 // when input reports are received. If simulateConnectFailure() was called 272 // then a null connection is returned instead, indicating failure. 273 async connect(guid, connectionClient) { 274 if (this.simulateConnectFailure_) { 275 this.simulateConnectFailure_ = false; 276 return {connection: null}; 277 } 278 const fakeConnection = new FakeHidConnection(connectionClient); 279 this.fakeConnections_.set(guid, fakeConnection); 280 return {connection: fakeConnection.bindNewPipeAndPassRemote()}; 281 } 282 283 // Removes the allowed device. 284 async forget(deviceInfo) { 285 for (const [key, value] of this.allowedDevices_) { 286 for (const device of value) { 287 if (device.guid == deviceInfo.guid) { 288 this.allowedDevices_.delete(key); 289 break; 290 } 291 } 292 } 293 return {success: true}; 294 } 295 } 296 297 export const fakeHidService = new FakeHidService();