mirror of
https://github.com/servo/servo.git
synced 2025-06-25 09:34:32 +01:00
437 lines
14 KiB
JavaScript
437 lines
14 KiB
JavaScript
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().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;
|
|
})();
|