<!DOCTYPE html>
<meta charset=utf-8>
<title>Web NFC: NDEFReader.write Tests</title>
<link rel="author" title="Intel" href="http://www.intel.com"/>
<link rel="help" href="https://w3c.github.io/web-nfc/"/>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/nfc-helpers.js"></script>
<script>

"use strict";

const invalid_type_messages =
    [
      // Invalid NDEFMessageSource type
      undefined,

      // NDEFMessage.records: should have at least 1 valid record.
      // https://w3c.github.io/web-nfc/#the-write-method - Step 8.
      createMessage([{}]),

      // NDEFMessageSource: not NDEF-formatable.
      // https://w3c.github.io/web-nfc/#the-write-method - Step 8.
      createMessage([]),

      // https://w3c.github.io/web-nfc/#dfn-map-text-to-ndef
      // NDEFRecord must have data.
      createMessage([createTextRecord()]),

      // NDEFRecord.data for 'text' record must be either a string,
      // an arrayBuffer, or an arrayBufferView.
      createMessage([createTextRecord(test_json_data)]),

      // NDEFRecord.encoding for 'text' record must be either "utf-8",
      // "utf-16", "utf-16le" or "utf-16be".
      createMessage([createTextRecord(test_buffer_data, "chinese")]),

      // https://w3c.github.io/web-nfc/#dfn-map-a-url-to-ndef
      // NDEFRecord must have data.
      createMessage([createUrlRecord()]),

      // https://w3c.github.io/web-nfc/#dfn-map-a-url-to-ndef
      // NDEFRecord must have data.
      createMessage([createUrlRecord(undefined, true)]),

      // NDEFRecord.data for 'url' record must be string.
      createMessage([createUrlRecord(test_buffer_data)]),
      createMessage([createUrlRecord(test_json_data)]),

      // NDEFRecord.data for 'absolute-url' record must be string.
      createMessage([createUrlRecord(test_buffer_data, true)]),
      createMessage([createUrlRecord(test_json_data, true)]),

      // https://w3c.github.io/web-nfc/#dfn-map-binary-data-to-ndef
      // NDEFRecord must have data.
      createMessage([createMimeRecord()]),

      // NDEFRecord.data for 'mime' record must be BufferSource.
      createMessage([createMimeRecord(test_text_data)]),
      createMessage([createMimeRecord(test_number_data)]),
      createMessage([createMimeRecord(test_json_data)]),

      // NDEFRecord must have data.
      createMessage([createUnknownRecord()]),

      // NDEFRecord.data for 'unknown' record must be BufferSource.
      createMessage([createUnknownRecord(test_text_data)]),
      createMessage([createUnknownRecord(test_number_data)]),
      createMessage([createUnknownRecord(test_json_data)]),

      // https://w3c.github.io/web-nfc/#dfn-map-external-data-to-ndef
      // NDEFRecord must have data.
      createMessage([createRecord('w3.org:xyz')]),

      // NDEFRecord.data for external record must be a BufferSource or NDEFMessageInit.
      createMessage([createRecord('w3.org:xyz', test_text_data)]),
      createMessage([createRecord('w3.org:xyz', test_number_data)]),
      createMessage([createRecord('w3.org:xyz', test_json_data)]),

      // https://w3c.github.io/web-nfc/#dfn-map-local-type-to-ndef
      // NDEFRecord must have data.
      createMessage([createRecord(':xyz')]),

      // NDEFRecord.data for local type record must be a BufferSource or NDEFMessageInit.
      createMessage([createRecord(':xyz', test_text_data)]),
      createMessage([createRecord(':xyz', test_number_data)]),
      createMessage([createRecord(':xyz', test_json_data)]),

      // https://w3c.github.io/web-nfc/#dfn-map-smart-poster-to-ndef
      // NDEFRecord must have data.
      createMessage([createRecord('smart-poster')]),

      // NDEFRecord.data for smart-poster record must be a NDEFMessageInit.
      createMessage([createRecord('smart-poster', test_text_data)]),
      createMessage([createRecord('smart-poster', test_number_data)]),
      createMessage([createRecord('smart-poster', test_json_data)]),
      createMessage([createRecord('smart-poster', test_buffer_data)]),

      // https://w3c.github.io/web-nfc/#ndef-record-types
      // The record type is neither a known type ('text', 'mime' etc.) nor a
      // valid external/local type.
      createMessage([createRecord('unmatched_type', test_buffer_data)])
    ];

const invalid_syntax_messages =
    [
      // Data for 'url' or 'absolute-url' record, must be a valid URL.
      createMessage([createUrlRecord('Invalid URL:// Data')]),
      createMessage([createUrlRecord('Invalid URL:// Data', true)]),

      // NDEFRecord.lang length for 'text' record must be lower than 64.
      createMessage([createTextRecord(test_text_data, undefined /* encoding */,
                                      [...Array(64)].map(_ => 'a'))]),
    ];

const invalid_signals = [
  "string",
  123,
  {},
  true,
  Symbol(),
  () => {},
  self
];

nfc_test(async t => {
  const ndef = new NDEFReader();
  const promises = [];
  invalid_type_messages.forEach(message => {
    promises.push(
      promise_rejects_js(t, TypeError, ndef.write(message)));
  });
  await Promise.all(promises);
}, "Test that promise is rejected with TypeError if NDEFMessageSource is invalid.");

nfc_test(async t => {
  const ndef = new NDEFReader();
  const promises = [];
  invalid_syntax_messages.forEach(message => {
    promises.push(
      promise_rejects_dom(t, 'SyntaxError', ndef.write(message)));
  });
  await Promise.all(promises);
}, "Test that promise is rejected with SyntaxError if NDEFMessageSource contains\
 invalid records.");

nfc_test(async t => {
  await test_driver.set_permission({ name: 'nfc' }, 'denied');
  const ndef = new NDEFReader();
  await promise_rejects_dom(t, 'NotAllowedError', ndef.write(test_text_data));
}, 'NDEFReader.write should fail if user permission is not granted.');

// We do not provide NFC mock here to simulate that there has no available
// implementation for NFC Mojo interface.
nfc_test(async (t, mockNFC) => {
  mockNFC.simulateClosedPipe();
  const ndef = new NDEFReader();
  await promise_rejects_dom(t, 'NotSupportedError', ndef.write(test_text_data));
}, 'NDEFReader.write should fail if no implementation for NFC Mojo interface is available.');

nfc_test(async (t, mockNFC) => {
  const ndef = new NDEFReader();
  const controller = new AbortController();

  //Make sure push is pending
  mockNFC.setPendingPushCompleted(false);
  const p = ndef.write(test_text_data, { signal: controller.signal });
  const rejected = promise_rejects_dom(t, 'AbortError', p);
  let callback_called = false;
  await new Promise(resolve => {
    t.step_timeout(() => {
      callback_called = true;
      controller.abort();
      resolve();
    }, 10);
  });
  await rejected;
  assert_true(callback_called, 'timeout should have caused the abort');
}, "NDEFReader.write should fail if abort write request before write happends.");

nfc_test(async t => {
  const ndef = new NDEFReader();
  const controller = new AbortController();
  assert_false(controller.signal.aborted);
  controller.abort();
  assert_true(controller.signal.aborted);
  await promise_rejects_dom(t, 'AbortError',
      ndef.write(test_text_data, { signal: controller.signal }));
}, "NDEFReader.write should fail if signal's aborted flag is set.");

nfc_test(async t => {
  const ndef = new NDEFReader();
  const promises = [];
  invalid_signals.forEach(invalid_signal => {
    promises.push(promise_rejects_js(t, TypeError,
        ndef.write(test_text_data, { signal: invalid_signal })));
  });
  await Promise.all(promises);
}, "NDEFReader.write should fail if signal is not an AbortSignal.");

nfc_test(async (t, mockNFC) => {
  const ndef1 = new NDEFReader();
  const ndef2 = new NDEFReader();
  const controller = new AbortController();
  const p1 = ndef1.write(test_text_data, { signal: controller.signal });

  // Even though write request is grantable,
  // this abort should be processed synchronously.
  controller.abort();
  await promise_rejects_dom(t, 'AbortError', p1);

  await ndef2.write(test_text_data);
  assertNDEFMessagesEqual(test_text_data, mockNFC.pushedMessage());
}, "Synchronously signaled abort.");

nfc_test(async (t, mockNFC) => {
  const ndef = new NDEFReader();
  mockNFC.setHWStatus(NFCHWStatus.DISABLED);
  await promise_rejects_dom(t, 'NotReadableError', ndef.write(test_text_data));
}, "NDEFReader.write should fail when NFC HW is disabled.");

nfc_test(async (t, mockNFC) => {
  const ndef = new NDEFReader();
  mockNFC.setHWStatus(NFCHWStatus.NOT_SUPPORTED);
  await promise_rejects_dom(t, 'NotSupportedError', ndef.write(test_text_data));
}, "NDEFReader.write should fail when NFC HW is not supported.");

nfc_test(async () => {
  await new Promise((resolve,reject) => {
    const iframe = document.createElement('iframe');
    iframe.srcdoc = `<script>
                      window.onmessage = async (message) => {
                        if (message.data === "Ready") {
                          try {
                            const ndef = new NDEFReader();
                            await ndef.write("Test");
                            parent.postMessage("Failure", "*");
                          } catch (error) {
                            if (error.name == "InvalidStateError") {
                              parent.postMessage("Success", "*");
                            } else {
                              parent.postMessage("Failure", "*");
                            }
                          }
                        }
                      };
                    <\/script>`;
    iframe.onload = () => iframe.contentWindow.postMessage('Ready', '*');
    document.body.appendChild(iframe);
    window.onmessage = message => {
      if (message.data == 'Success') {
        resolve();
      } else if (message.data == 'Failure') {
        reject();
      }
    }
  });
}, 'Test that WebNFC API is not accessible from iframe context.');

nfc_test(async () => {
  const ndef = new NDEFReader();
  await ndef.write(test_text_data);
}, 'NDEFReader.write should succeed when NFC HW is enabled');

nfc_test(async (t, mockNFC) => {
  const ndef = new NDEFReader();
  const message = createMessage([createTextRecord(test_text_data),
                               createMimeRecordFromJson(test_json_data),
                               createMimeRecord(test_buffer_data),
                               createUnknownRecord(test_buffer_data),
                               createUrlRecord(test_url_data),
                               createUrlRecord(test_url_data, true),
                               createRecord('w3.org:xyz', test_buffer_data)],
                               test_message_origin);
  await ndef.write(message);
  assertNDEFMessagesEqual(message, mockNFC.pushedMessage());
}, "NDEFReader.write NDEFMessage containing text, mime, unknown, url, absolute-url \
and external records with default NDEFWriteOptions.");

nfc_test(async (t, mockNFC) => {
  const messageContainText = createMessage([createTextRecord(test_text_data)]);

  // Prepare a local type record that uses |messageContainText| as its payload.
  const messageContainLocal = createMessage([createRecord(':containsTextRecord', messageContainText)]);

  // Prepare an external type record that uses |messageContainLocal| as its payload.
  const message = createMessage([createRecord('example.com:containsLocalRecord', messageContainLocal)]);

  const ndef = new NDEFReader();
  await ndef.write(message);
  const pushed_message = mockNFC.pushedMessage();

  // The mojom message received by mock nfc contains only the external type record.
  assert_equals(pushed_message.data.length, 1);
  assert_equals(pushed_message.data[0].recordType, 'example.com:containsLocalRecord', 'recordType');

  // The external type record's payload is from the original |messageContainLocal|,
  // containing only the local type record.
  assert_array_equals(pushed_message.data[0].data, new Uint8Array(0),
      'payloadMessage is used instead');
  assert_equals(pushed_message.data[0].payloadMessage.data.length, 1);
  assert_equals(pushed_message.data[0].payloadMessage.data[0].recordType, ':containsTextRecord', 'recordType');

  // The local type record's payload is from the original |messageContainText|,
  // containing only the text record.
  assert_array_equals(pushed_message.data[0].payloadMessage.data[0].data, new Uint8Array(0),
      'payloadMessage is used instead');
  assertNDEFMessagesEqual(messageContainText, pushed_message.data[0].payloadMessage.data[0].payloadMessage);
}, "NDEFReader.write NDEFMessage containing embedded records.");

nfc_test(async (t, mockNFC) => {
  // A smart-poster record contains a uri record, text record.
  const uri_record = createUrlRecord(test_url_data);
  const text_record = createTextRecord(test_text_data);
  const payload_message = createMessage([uri_record, text_record]);
  const message = createMessage([createRecord(
      'smart-poster', payload_message, "dummy_record_id")]);

  const ndef = new NDEFReader();
  await ndef.write(message);
  const pushed_message = mockNFC.pushedMessage();

  // The mojom message received by mock nfc contains only the smart-poster record.
  assert_equals(pushed_message.data.length, 1);
  assert_equals(pushed_message.data[0].recordType, 'smart-poster', 'recordType');
  assert_equals(pushed_message.data[0].mediaType, null, 'mediaType');
  assert_equals(pushed_message.data[0].id, 'dummy_record_id', 'id');

  const embedded_records = pushed_message.data[0].payloadMessage.data;
  assert_equals(embedded_records.length, 2);

  let embedded_record_types = [];
  for(let record of embedded_records) {
    embedded_record_types.push(record.recordType);
    switch(record.recordType) {
      case 'url':
        compareNDEFRecords(uri_record, record);
        break;
      case 'text':
        compareNDEFRecords(text_record, record);
        break;
      default:
        assert_unreached("Unknown recordType");
    }
  }
  assert_array_equals(embedded_record_types.sort(), ['text', 'url'],
      'smart-poster record\'s contained record types');
}, "NDEFReader.write NDEFMessage containing smart-poster record.");

nfc_test(async (t, mockNFC) => {
  const ndef = new NDEFReader();
  await ndef.write(test_text_data);
  assertNDEFMessagesEqual(test_text_data, mockNFC.pushedMessage());
}, "Test that NDEFReader.write succeeds when message is DOMString.");

nfc_test(async (t, mockNFC) => {
  const ndef = new NDEFReader();
  await ndef.write(test_buffer_data);
  assertNDEFMessagesEqual(test_buffer_data, mockNFC.pushedMessage());
}, "Test that NDEFReader.write succeeds when message is ArrayBuffer.");

nfc_test(async (t, mockNFC) => {
  let buffer_view = new Uint8Array(test_buffer_data, 2, 5);
  const ndef = new NDEFReader();
  await ndef.write(buffer_view);
  assertNDEFMessagesEqual(buffer_view, mockNFC.pushedMessage());
}, "Test that NDEFReader.write succeeds when message is ArrayBufferView.");

nfc_test(async (t, mockNFC) => {
  const ndef = new NDEFReader();
  await ndef.write(createMessage([createRecord('empty')]));
  const receivedMessage = mockNFC.pushedMessage();
  assert_equals(receivedMessage.data.length, 1);
  assert_equals(receivedMessage.data[0].recordType, 'empty', 'recordType');
}, "NDEFReader.write with 'empty' record should succeed.");

nfc_test(async (t, mockNFC) => {
  const ndef = new NDEFReader();
  await ndef.write(test_text_data);
  assertNDEFWriteOptionsEqual({overwrite: true}, mockNFC.writeOptions());
}, "Check that default NDEFWriteOptions values are correctly set.");

nfc_test(async (t, mockNFC) => {
  const ndef = new NDEFReader();
  await ndef.write(test_text_data, {overwrite: false});
  assertNDEFWriteOptionsEqual({overwrite: false}, mockNFC.writeOptions());
}, "Check that provided NDEFWriteOptions values are correctly converted.");

nfc_test(async (t, mockNFC) => {
  const ndef1 = new NDEFReader();
  const ndef2 = new NDEFReader();

  const p1 = ndef1.write(test_text_data, {overwrite: false});
  const p2 = ndef2.write(test_url_data, {overwrite: true});

  await new Promise((resolve, reject) => {
    // Make first push pending
    mockNFC.setPendingPushCompleted(false);
    let err = "";
    p1.then(() => {
      reject("pending push should not be fulfilled");
    }).catch(e => {
      err = e.name;
    });
    p2.then(() => {
      assertNDEFMessagesEqual(test_url_data, mockNFC.pushedMessage());
      assertNDEFWriteOptionsEqual( {overwrite: true}, mockNFC.writeOptions());
      assert_equals(err, "AbortError", "the pending push should be aborted");
      resolve();
    });
  });
}, "NDEFReader.write should replace all previously configured write operations.");

nfc_test(async () => {
  const ndef = new NDEFReader();

  const controller1 = new AbortController();
  await ndef.write(test_text_data, {signal: controller1.signal});

  const controller2 = new AbortController();
  const promise = ndef.write(test_text_data, {signal: controller2.signal});
  controller1.abort();
  await promise;
}, 'NDEFReader.write signals are independent.');

nfc_test(async (t, mockNFC) => {
  const ndef = new NDEFReader();
  await ndef.write({ records: [{ recordType: "mime", data: test_buffer_data }] });
  assertNDEFMessagesEqual(test_buffer_data, mockNFC.pushedMessage());
}, "Test that mediaType should be set to 'application/octet-stream' if \
NDEFRecordInit.record's recordType is 'mime' and NDEFRecordInit.record's \
mediaType is undefined.");

nfc_test(async (t, mockNFC) => {
  // Make sure the push will be pending in the mock.
  mockNFC.setPendingPushCompleted(false);

  const ndef1 = new NDEFReader();
  const promise = ndef1.write(test_text_data);

  // Just to make sure the write() request has already reached to the mock.
  const ndef2 = new NDEFReader();
  await ndef2.scan();

  mockNFC.simulateNonNDEFTagDiscovered();
  await promise_rejects_dom(t, 'NotSupportedError', promise);
}, "NDEFReader.write should fail when the NFC device coming up does not expose \
NDEF technology.");

nfc_test(async (t, mockNFC) => {
  const ndef = new NDEFReader();
  await ndef.write(test_text_data, {overwrite: false});
  assertNDEFMessagesEqual(test_text_data, mockNFC.pushedMessage());
}, "NDEFReader.write should succeed to write data to an unformatted NFC device \
when the NDEFWriteOptions.overwrite is false.");

nfc_test(async (t, mockNFC) => {
  const ndef = new NDEFReader();
  await ndef.write(test_buffer_data);
  assertNDEFMessagesEqual(test_buffer_data, mockNFC.pushedMessage());
  await ndef.write(test_text_data, {overwrite: true});
  assertNDEFMessagesEqual(test_text_data, mockNFC.pushedMessage());
}, "NDEFReader.write should succeed to overwrite the existing data \
when the NDEFWriteOptions.overwrite is true.");

nfc_test(async (t, mockNFC) => {
  const ndef = new NDEFReader();
  const p = ndef.write(test_text_data, {overwrite: false});
  mockNFC.setIsFormattedTag(true);
  await promise_rejects_dom(t, 'NotAllowedError', p);
}, "NDEFReader.write should fail when there are NDEF records on the NFC device \
and NDEFWriteOptions.overwrite is false.");

nfc_test(async (t, mockNFC) => {
  const ndef = new NDEFReader();
  mockNFC.simulateDataTransferFails();
  await promise_rejects_dom(t, 'NetworkError', ndef.write(test_text_data));
}, "NDEFReader.write should fail with NetworkError when NFC data transfer fails.");

</script>
