import {NDEFErrorType, NDEFRecordTypeCategory, NFC, NFCReceiver} from '/gen/services/device/public/mojom/nfc.mojom.m.js'; // Converts between NDEFMessageInit https://w3c.github.io/web-nfc/#dom-ndefmessage // and mojom.NDEFMessage structure, so that watch function can be tested. function toMojoNDEFMessage(message) { let ndefMessage = {data: []}; for (let record of message.records) { ndefMessage.data.push(toMojoNDEFRecord(record)); } return ndefMessage; } function toMojoNDEFRecord(record) { let nfcRecord = {}; // Simply checks the existence of ':' to decide whether it's an external // type or a local type. As a mock, no need to really implement the validation // algorithms for them. if (record.recordType.startsWith(':')) { nfcRecord.category = NDEFRecordTypeCategory.kLocal; } else if (record.recordType.search(':') != -1) { nfcRecord.category = NDEFRecordTypeCategory.kExternal; } else { nfcRecord.category = NDEFRecordTypeCategory.kStandardized; } nfcRecord.recordType = record.recordType; nfcRecord.mediaType = record.mediaType; nfcRecord.id = record.id; if (record.recordType == 'text') { nfcRecord.encoding = record.encoding == null? 'utf-8': record.encoding; nfcRecord.lang = record.lang == null? 'en': record.lang; } nfcRecord.data = toByteArray(record.data); if (record.data != null && record.data.records !== undefined) { // |record.data| may be an NDEFMessageInit, i.e. the payload is a message. nfcRecord.payloadMessage = toMojoNDEFMessage(record.data); } return nfcRecord; } // Converts JS objects to byte array. function toByteArray(data) { if (data instanceof ArrayBuffer) return new Uint8Array(data); else if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); let byteArray = new Uint8Array(0); let tmpData = data; if (typeof tmpData === 'object' || typeof tmpData === 'number') tmpData = JSON.stringify(tmpData); if (typeof tmpData === 'string') byteArray = new TextEncoder('utf-8').encode(tmpData); return byteArray; } // Compares NDEFRecords that were provided / received by the mock service. // TODO: Use different getters to get received record data, // see spec changes at https://github.com/w3c/web-nfc/pull/243. self.compareNDEFRecords = function(providedRecord, receivedRecord) { assert_equals(providedRecord.recordType, receivedRecord.recordType); if (providedRecord.id === undefined) { assert_equals(null, receivedRecord.id); } else { assert_equals(providedRecord.id, receivedRecord.id); } if (providedRecord.mediaType === undefined) { assert_equals(null, receivedRecord.mediaType); } else { assert_equals(providedRecord.mediaType, receivedRecord.mediaType); } assert_not_equals(providedRecord.recordType, 'empty'); if (providedRecord.recordType == 'text') { assert_equals( providedRecord.encoding == null? 'utf-8': providedRecord.encoding, receivedRecord.encoding); assert_equals(providedRecord.lang == null? 'en': providedRecord.lang, receivedRecord.lang); } else { assert_equals(null, receivedRecord.encoding); assert_equals(null, receivedRecord.lang); } assert_array_equals(toByteArray(providedRecord.data), new Uint8Array(receivedRecord.data)); } // Compares NDEFWriteOptions structures that were provided to API and // received by the mock mojo service. self.assertNDEFWriteOptionsEqual = function(provided, received) { if (provided.overwrite !== undefined) assert_equals(provided.overwrite, !!received.overwrite); else assert_equals(!!received.overwrite, true); } // Compares NDEFReaderOptions structures that were provided to API and // received by the mock mojo service. self.assertNDEFReaderOptionsEqual = function(provided, received) { if (provided.url !== undefined) assert_equals(provided.url, received.url); else assert_equals(received.url, ''); if (provided.mediaType !== undefined) assert_equals(provided.mediaType, received.mediaType); else assert_equals(received.mediaType, ''); if (provided.recordType !== undefined) { assert_equals(provided.recordType, received.recordType); } } function createNDEFError(type) { return {error: (type != null ? {errorType: type, errorMessage: ''} : null)}; } self.WebNFCTest = (() => { class MockNFC { constructor() { this.receiver_ = new NFCReceiver(this); this.interceptor_ = new MojoInterfaceInterceptor(NFC.$interfaceName); this.interceptor_.oninterfacerequest = e => { if (this.should_close_pipe_on_request_) e.handle.close(); else this.receiver_.$.bindHandle(e.handle); } this.interceptor_.start(); this.hw_status_ = NFCHWStatus.ENABLED; this.pushed_message_ = null; this.pending_write_options_ = null; this.pending_push_promise_func_ = null; this.push_completed_ = true; this.pending_make_read_only_promise_func_ = null; this.make_read_only_completed_ = true; this.client_ = null; this.watchers_ = []; this.reading_messages_ = []; this.operations_suspended_ = false; this.is_formatted_tag_ = false; this.data_transfer_failed_ = false; this.should_close_pipe_on_request_ = false; } // NFC delegate functions. async push(message, options) { const error = this.getHWError(); if (error) return error; // Cancels previous pending push operation. if (this.pending_push_promise_func_) { this.cancelPendingPushOperation(); } this.pushed_message_ = message; this.pending_write_options_ = options; return new Promise(resolve => { if (this.operations_suspended_ || !this.push_completed_) { // Leaves the push pending. this.pending_push_promise_func_ = resolve; } else if (this.is_formatted_tag_ && !options.overwrite) { // Resolves with NotAllowedError if there are NDEF records on the device // and overwrite is false. resolve(createNDEFError(NDEFErrorType.NOT_ALLOWED)); } else if (this.data_transfer_failed_) { // Resolves with NetworkError if data transfer fails. resolve(createNDEFError(NDEFErrorType.IO_ERROR)); } else { resolve(createNDEFError(null)); } }); } async cancelPush() { this.cancelPendingPushOperation(); return createNDEFError(null); } async makeReadOnly(options) { const error = this.getHWError(); if (error) return error; // Cancels previous pending makeReadOnly operation. if (this.pending_make_read_only_promise_func_) { this.cancelPendingMakeReadOnlyOperation(); } if (this.operations_suspended_ || !this.make_read_only_completed_) { // Leaves the makeReadOnly pending. return new Promise(resolve => { this.pending_make_read_only_promise_func_ = resolve; }); } else if (this.data_transfer_failed_) { // Resolves with NetworkError if data transfer fails. return createNDEFError(NDEFErrorType.IO_ERROR); } else { return createNDEFError(null); } } async cancelMakeReadOnly() { this.cancelPendingMakeReadOnlyOperation(); return createNDEFError(null); } setClient(client) { this.client_ = client; } async watch(id) { assert_true(id > 0); const error = this.getHWError(); if (error) { return error; } this.watchers_.push({id: id}); // Ignores reading if NFC operation is suspended // or the NFC tag does not expose NDEF technology. if (!this.operations_suspended_) { // Triggers onWatch if the new watcher matches existing messages. for (let message of this.reading_messages_) { this.client_.onWatch( [id], fake_tag_serial_number, toMojoNDEFMessage(message)); } } return createNDEFError(null); } cancelWatch(id) { let index = this.watchers_.findIndex(value => value.id === id); if (index !== -1) { this.watchers_.splice(index, 1); } } getHWError() { if (this.hw_status_ === NFCHWStatus.DISABLED) return createNDEFError(NDEFErrorType.NOT_READABLE); if (this.hw_status_ === NFCHWStatus.NOT_SUPPORTED) return createNDEFError(NDEFErrorType.NOT_SUPPORTED); return null; } setHWStatus(status) { this.hw_status_ = status; } pushedMessage() { return this.pushed_message_; } writeOptions() { return this.pending_write_options_; } watchOptions() { assert_not_equals(this.watchers_.length, 0); return this.watchers_[this.watchers_.length - 1].options; } setPendingPushCompleted(result) { this.push_completed_ = result; } setPendingMakeReadOnlyCompleted(result) { this.make_read_only_completed_ = result; } reset() { this.hw_status_ = NFCHWStatus.ENABLED; this.watchers_ = []; this.reading_messages_ = []; this.operations_suspended_ = false; this.cancelPendingPushOperation(); this.cancelPendingMakeReadOnlyOperation(); this.is_formatted_tag_ = false; this.data_transfer_failed_ = false; this.should_close_pipe_on_request_ = false; } cancelPendingPushOperation() { if (this.pending_push_promise_func_) { this.pending_push_promise_func_( createNDEFError(NDEFErrorType.OPERATION_CANCELLED)); this.pending_push_promise_func_ = null; } this.pushed_message_ = null; this.pending_write_options_ = null; this.push_completed_ = true; } cancelPendingMakeReadOnlyOperation() { if (this.pending_make_read_only_promise_func_) { this.pending_make_read_only_promise_func_( createNDEFError(NDEFErrorType.OPERATION_CANCELLED)); this.pending_make_read_only_promise_func_ = null; } this.make_read_only_completed_ = true; } // Sets message that is used to deliver NFC reading updates. setReadingMessage(message) { this.reading_messages_.push(message); // Ignores reading if NFC operation is suspended. if(this.operations_suspended_) return; // when overwrite is false, the write algorithm will read the NFC tag // to determine if it has NDEF records on it. if (this.pending_write_options_ && this.pending_write_options_.overwrite) return; // Triggers onWatch if the new message matches existing watchers. for (let watcher of this.watchers_) { this.client_.onWatch( [watcher.id], fake_tag_serial_number, toMojoNDEFMessage(message)); } } // Suspends all pending NFC operations. Could be used when web page // visibility is lost. suspendNFCOperations() { this.operations_suspended_ = true; } // Resumes all suspended NFC operations. resumeNFCOperations() { this.operations_suspended_ = false; // Resumes pending NFC reading. for (let watcher of this.watchers_) { for (let message of this.reading_messages_) { this.client_.onWatch( [watcher.id], fake_tag_serial_number, toMojoNDEFMessage(message)); } } // Resumes pending push operation. if (this.pending_push_promise_func_ && this.push_completed_) { this.pending_push_promise_func_(createNDEFError(null)); this.pending_push_promise_func_ = null; } // Resumes pending makeReadOnly operation. if (this.pending_make_read_only_promise_func_ && this.make_read_only_completed_) { this.pending_make_read_only_promise_func_(createNDEFError(null)); this.pending_make_read_only_promise_func_ = null; } } // Simulates the device coming in proximity does not expose NDEF technology. simulateNonNDEFTagDiscovered() { // Notify NotSupportedError to all active readers. if (this.watchers_.length != 0) { this.client_.onError({ errorType: NDEFErrorType.NOT_SUPPORTED, errorMessage: '' }); } // Reject the pending push with NotSupportedError. if (this.pending_push_promise_func_) { this.pending_push_promise_func_( createNDEFError(NDEFErrorType.NOT_SUPPORTED)); this.pending_push_promise_func_ = null; } // Reject the pending makeReadOnly with NotSupportedError. if (this.pending_make_read_only_promise_func_) { this.pending_make_read_only_promise_func_( createNDEFError(NDEFErrorType.NOT_SUPPORTED)); this.pending_make_read_only_promise_func_ = null; } } setIsFormattedTag(isFormatted) { this.is_formatted_tag_ = isFormatted; } simulateDataTransferFails() { this.data_transfer_failed_ = true; } simulateClosedPipe() { this.should_close_pipe_on_request_ = true; } } let testInternal = { initialized: false, mockNFC: null } class NFCTestChromium { constructor() { Object.freeze(this); // Makes it immutable. } async initialize() { if (testInternal.initialized) throw new Error('Call reset() before initialize().'); // Grant nfc permissions for Chromium testdriver. await test_driver.set_permission({ name: 'nfc' }, 'granted'); if (testInternal.mockNFC == null) { testInternal.mockNFC = new MockNFC(); } testInternal.initialized = true; } // Reuses the nfc mock but resets its state between test runs. async reset() { if (!testInternal.initialized) throw new Error('Call initialize() before reset().'); testInternal.mockNFC.reset(); testInternal.initialized = false; await new Promise(resolve => setTimeout(resolve, 0)); } getMockNFC() { return testInternal.mockNFC; } } return NFCTestChromium; })();