nfc-mock.js (14131B)
1 import {NDEFErrorType, NDEFRecordTypeCategory, NFC, NFCReceiver} from '/gen/services/device/public/mojom/nfc.mojom.m.js'; 2 3 // Converts between NDEFMessageInit https://w3c.github.io/web-nfc/#dom-ndefmessage 4 // and mojom.NDEFMessage structure, so that watch function can be tested. 5 function toMojoNDEFMessage(message) { 6 let ndefMessage = {data: []}; 7 for (let record of message.records) { 8 ndefMessage.data.push(toMojoNDEFRecord(record)); 9 } 10 return ndefMessage; 11 } 12 13 function toMojoNDEFRecord(record) { 14 let nfcRecord = {}; 15 // Simply checks the existence of ':' to decide whether it's an external 16 // type or a local type. As a mock, no need to really implement the validation 17 // algorithms for them. 18 if (record.recordType.startsWith(':')) { 19 nfcRecord.category = NDEFRecordTypeCategory.kLocal; 20 } else if (record.recordType.search(':') != -1) { 21 nfcRecord.category = NDEFRecordTypeCategory.kExternal; 22 } else { 23 nfcRecord.category = NDEFRecordTypeCategory.kStandardized; 24 } 25 nfcRecord.recordType = record.recordType; 26 nfcRecord.mediaType = record.mediaType; 27 nfcRecord.id = record.id; 28 if (record.recordType == 'text') { 29 nfcRecord.encoding = record.encoding == null? 'utf-8': record.encoding; 30 nfcRecord.lang = record.lang == null? 'en': record.lang; 31 } 32 nfcRecord.data = toByteArray(record.data); 33 if (record.data != null && record.data.records !== undefined) { 34 // |record.data| may be an NDEFMessageInit, i.e. the payload is a message. 35 nfcRecord.payloadMessage = toMojoNDEFMessage(record.data); 36 } 37 return nfcRecord; 38 } 39 40 // Converts JS objects to byte array. 41 function toByteArray(data) { 42 if (data instanceof ArrayBuffer) 43 return new Uint8Array(data); 44 else if (ArrayBuffer.isView(data)) 45 return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); 46 47 let byteArray = new Uint8Array(0); 48 let tmpData = data; 49 if (typeof tmpData === 'object' || typeof tmpData === 'number') 50 tmpData = JSON.stringify(tmpData); 51 52 if (typeof tmpData === 'string') 53 byteArray = new TextEncoder().encode(tmpData); 54 55 return byteArray; 56 } 57 58 // Compares NDEFRecords that were provided / received by the mock service. 59 // TODO: Use different getters to get received record data, 60 // see spec changes at https://github.com/w3c/web-nfc/pull/243. 61 self.compareNDEFRecords = function(providedRecord, receivedRecord) { 62 assert_equals(providedRecord.recordType, receivedRecord.recordType); 63 64 if (providedRecord.id === undefined) { 65 assert_equals(null, receivedRecord.id); 66 } else { 67 assert_equals(providedRecord.id, receivedRecord.id); 68 } 69 70 if (providedRecord.mediaType === undefined) { 71 assert_equals(null, receivedRecord.mediaType); 72 } else { 73 assert_equals(providedRecord.mediaType, receivedRecord.mediaType); 74 } 75 76 assert_not_equals(providedRecord.recordType, 'empty'); 77 78 if (providedRecord.recordType == 'text') { 79 assert_equals( 80 providedRecord.encoding == null? 'utf-8': providedRecord.encoding, 81 receivedRecord.encoding); 82 assert_equals(providedRecord.lang == null? 'en': providedRecord.lang, 83 receivedRecord.lang); 84 } else { 85 assert_equals(null, receivedRecord.encoding); 86 assert_equals(null, receivedRecord.lang); 87 } 88 89 assert_array_equals(toByteArray(providedRecord.data), 90 new Uint8Array(receivedRecord.data)); 91 } 92 93 // Compares NDEFWriteOptions structures that were provided to API and 94 // received by the mock mojo service. 95 self.assertNDEFWriteOptionsEqual = function(provided, received) { 96 if (provided.overwrite !== undefined) 97 assert_equals(provided.overwrite, !!received.overwrite); 98 else 99 assert_equals(!!received.overwrite, true); 100 } 101 102 // Compares NDEFReaderOptions structures that were provided to API and 103 // received by the mock mojo service. 104 self.assertNDEFReaderOptionsEqual = function(provided, received) { 105 if (provided.url !== undefined) 106 assert_equals(provided.url, received.url); 107 else 108 assert_equals(received.url, ''); 109 110 if (provided.mediaType !== undefined) 111 assert_equals(provided.mediaType, received.mediaType); 112 else 113 assert_equals(received.mediaType, ''); 114 115 if (provided.recordType !== undefined) { 116 assert_equals(provided.recordType, received.recordType); 117 } 118 } 119 120 function createNDEFError(type) { 121 return {error: (type != null ? {errorType: type, errorMessage: ''} : null)}; 122 } 123 124 self.WebNFCTest = (() => { 125 class MockNFC { 126 constructor() { 127 this.receiver_ = new NFCReceiver(this); 128 129 this.interceptor_ = new MojoInterfaceInterceptor(NFC.$interfaceName); 130 this.interceptor_.oninterfacerequest = e => { 131 if (this.should_close_pipe_on_request_) 132 e.handle.close(); 133 else 134 this.receiver_.$.bindHandle(e.handle); 135 } 136 137 this.interceptor_.start(); 138 139 this.hw_status_ = NFCHWStatus.ENABLED; 140 this.pushed_message_ = null; 141 this.pending_write_options_ = null; 142 this.pending_push_promise_func_ = null; 143 this.push_completed_ = true; 144 this.pending_make_read_only_promise_func_ = null; 145 this.make_read_only_completed_ = true; 146 this.client_ = null; 147 this.watchers_ = []; 148 this.reading_messages_ = []; 149 this.operations_suspended_ = false; 150 this.is_formatted_tag_ = false; 151 this.data_transfer_failed_ = false; 152 this.should_close_pipe_on_request_ = false; 153 } 154 155 // NFC delegate functions. 156 async push(message, options) { 157 const error = this.getHWError(); 158 if (error) 159 return error; 160 // Cancels previous pending push operation. 161 if (this.pending_push_promise_func_) { 162 this.cancelPendingPushOperation(); 163 } 164 165 this.pushed_message_ = message; 166 this.pending_write_options_ = options; 167 return new Promise(resolve => { 168 if (this.operations_suspended_ || !this.push_completed_) { 169 // Leaves the push pending. 170 this.pending_push_promise_func_ = resolve; 171 } else if (this.is_formatted_tag_ && !options.overwrite) { 172 // Resolves with NotAllowedError if there are NDEF records on the device 173 // and overwrite is false. 174 resolve(createNDEFError(NDEFErrorType.NOT_ALLOWED)); 175 } else if (this.data_transfer_failed_) { 176 // Resolves with NetworkError if data transfer fails. 177 resolve(createNDEFError(NDEFErrorType.IO_ERROR)); 178 } else { 179 resolve(createNDEFError(null)); 180 } 181 }); 182 } 183 184 async cancelPush() { 185 this.cancelPendingPushOperation(); 186 return createNDEFError(null); 187 } 188 189 async makeReadOnly(options) { 190 const error = this.getHWError(); 191 if (error) 192 return error; 193 // Cancels previous pending makeReadOnly operation. 194 if (this.pending_make_read_only_promise_func_) { 195 this.cancelPendingMakeReadOnlyOperation(); 196 } 197 198 if (this.operations_suspended_ || !this.make_read_only_completed_) { 199 // Leaves the makeReadOnly pending. 200 return new Promise(resolve => { 201 this.pending_make_read_only_promise_func_ = resolve; 202 }); 203 } else if (this.data_transfer_failed_) { 204 // Resolves with NetworkError if data transfer fails. 205 return createNDEFError(NDEFErrorType.IO_ERROR); 206 } else { 207 return createNDEFError(null); 208 } 209 } 210 211 async cancelMakeReadOnly() { 212 this.cancelPendingMakeReadOnlyOperation(); 213 return createNDEFError(null); 214 } 215 216 setClient(client) { 217 this.client_ = client; 218 } 219 220 async watch(id) { 221 assert_true(id > 0); 222 const error = this.getHWError(); 223 if (error) { 224 return error; 225 } 226 227 this.watchers_.push({id: id}); 228 // Ignores reading if NFC operation is suspended 229 // or the NFC tag does not expose NDEF technology. 230 if (!this.operations_suspended_) { 231 // Triggers onWatch if the new watcher matches existing messages. 232 for (let message of this.reading_messages_) { 233 this.client_.onWatch( 234 [id], fake_tag_serial_number, toMojoNDEFMessage(message)); 235 } 236 } 237 238 return createNDEFError(null); 239 } 240 241 cancelWatch(id) { 242 let index = this.watchers_.findIndex(value => value.id === id); 243 if (index !== -1) { 244 this.watchers_.splice(index, 1); 245 } 246 } 247 248 getHWError() { 249 if (this.hw_status_ === NFCHWStatus.DISABLED) 250 return createNDEFError(NDEFErrorType.NOT_READABLE); 251 if (this.hw_status_ === NFCHWStatus.NOT_SUPPORTED) 252 return createNDEFError(NDEFErrorType.NOT_SUPPORTED); 253 return null; 254 } 255 256 setHWStatus(status) { 257 this.hw_status_ = status; 258 } 259 260 pushedMessage() { 261 return this.pushed_message_; 262 } 263 264 writeOptions() { 265 return this.pending_write_options_; 266 } 267 268 watchOptions() { 269 assert_not_equals(this.watchers_.length, 0); 270 return this.watchers_[this.watchers_.length - 1].options; 271 } 272 273 setPendingPushCompleted(result) { 274 this.push_completed_ = result; 275 } 276 277 setPendingMakeReadOnlyCompleted(result) { 278 this.make_read_only_completed_ = result; 279 } 280 281 reset() { 282 this.hw_status_ = NFCHWStatus.ENABLED; 283 this.watchers_ = []; 284 this.reading_messages_ = []; 285 this.operations_suspended_ = false; 286 this.cancelPendingPushOperation(); 287 this.cancelPendingMakeReadOnlyOperation(); 288 this.is_formatted_tag_ = false; 289 this.data_transfer_failed_ = false; 290 this.should_close_pipe_on_request_ = false; 291 } 292 293 cancelPendingPushOperation() { 294 if (this.pending_push_promise_func_) { 295 this.pending_push_promise_func_( 296 createNDEFError(NDEFErrorType.OPERATION_CANCELLED)); 297 this.pending_push_promise_func_ = null; 298 } 299 300 this.pushed_message_ = null; 301 this.pending_write_options_ = null; 302 this.push_completed_ = true; 303 } 304 305 cancelPendingMakeReadOnlyOperation() { 306 if (this.pending_make_read_only_promise_func_) { 307 this.pending_make_read_only_promise_func_( 308 createNDEFError(NDEFErrorType.OPERATION_CANCELLED)); 309 this.pending_make_read_only_promise_func_ = null; 310 } 311 312 this.make_read_only_completed_ = true; 313 } 314 315 // Sets message that is used to deliver NFC reading updates. 316 setReadingMessage(message) { 317 this.reading_messages_.push(message); 318 // Ignores reading if NFC operation is suspended. 319 if(this.operations_suspended_) return; 320 // when overwrite is false, the write algorithm will read the NFC tag 321 // to determine if it has NDEF records on it. 322 if (this.pending_write_options_ && this.pending_write_options_.overwrite) 323 return; 324 // Triggers onWatch if the new message matches existing watchers. 325 for (let watcher of this.watchers_) { 326 this.client_.onWatch( 327 [watcher.id], fake_tag_serial_number, 328 toMojoNDEFMessage(message)); 329 } 330 } 331 332 // Suspends all pending NFC operations. Could be used when web page 333 // visibility is lost. 334 suspendNFCOperations() { 335 this.operations_suspended_ = true; 336 } 337 338 // Resumes all suspended NFC operations. 339 resumeNFCOperations() { 340 this.operations_suspended_ = false; 341 // Resumes pending NFC reading. 342 for (let watcher of this.watchers_) { 343 for (let message of this.reading_messages_) { 344 this.client_.onWatch( 345 [watcher.id], fake_tag_serial_number, 346 toMojoNDEFMessage(message)); 347 } 348 } 349 // Resumes pending push operation. 350 if (this.pending_push_promise_func_ && this.push_completed_) { 351 this.pending_push_promise_func_(createNDEFError(null)); 352 this.pending_push_promise_func_ = null; 353 } 354 // Resumes pending makeReadOnly operation. 355 if (this.pending_make_read_only_promise_func_ && 356 this.make_read_only_completed_) { 357 this.pending_make_read_only_promise_func_(createNDEFError(null)); 358 this.pending_make_read_only_promise_func_ = null; 359 } 360 } 361 362 // Simulates the device coming in proximity does not expose NDEF technology. 363 simulateNonNDEFTagDiscovered() { 364 // Notify NotSupportedError to all active readers. 365 if (this.watchers_.length != 0) { 366 this.client_.onError({ 367 errorType: NDEFErrorType.NOT_SUPPORTED, 368 errorMessage: '' 369 }); 370 } 371 // Reject the pending push with NotSupportedError. 372 if (this.pending_push_promise_func_) { 373 this.pending_push_promise_func_( 374 createNDEFError(NDEFErrorType.NOT_SUPPORTED)); 375 this.pending_push_promise_func_ = null; 376 } 377 // Reject the pending makeReadOnly with NotSupportedError. 378 if (this.pending_make_read_only_promise_func_) { 379 this.pending_make_read_only_promise_func_( 380 createNDEFError(NDEFErrorType.NOT_SUPPORTED)); 381 this.pending_make_read_only_promise_func_ = null; 382 } 383 } 384 385 setIsFormattedTag(isFormatted) { 386 this.is_formatted_tag_ = isFormatted; 387 } 388 389 simulateDataTransferFails() { 390 this.data_transfer_failed_ = true; 391 } 392 393 simulateClosedPipe() { 394 this.should_close_pipe_on_request_ = true; 395 } 396 } 397 398 let testInternal = { 399 initialized: false, 400 mockNFC: null 401 } 402 403 class NFCTestChromium { 404 constructor() { 405 Object.freeze(this); // Makes it immutable. 406 } 407 408 async initialize() { 409 if (testInternal.initialized) 410 throw new Error('Call reset() before initialize().'); 411 412 // Grant nfc permissions for Chromium testdriver. 413 await test_driver.set_permission({ name: 'nfc' }, 'granted'); 414 415 if (testInternal.mockNFC == null) { 416 testInternal.mockNFC = new MockNFC(); 417 } 418 testInternal.initialized = true; 419 } 420 421 // Reuses the nfc mock but resets its state between test runs. 422 async reset() { 423 if (!testInternal.initialized) 424 throw new Error('Call initialize() before reset().'); 425 testInternal.mockNFC.reset(); 426 testInternal.initialized = false; 427 428 await new Promise(resolve => setTimeout(resolve, 0)); 429 } 430 431 getMockNFC() { 432 return testInternal.mockNFC; 433 } 434 } 435 436 return NFCTestChromium; 437 })();