<!DOCTYPE HTML>
<html>
<head>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=414796
-->
  <title>Test for Bug 414796</title>
  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>

<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=414796">Mozilla Bug 414796</a>
<p id="display">
</p>
<div id="content" style="display: none">
</div>

<pre id="test">
<script class="testbody" type="text/javascript;version=1.7">

// File constructors should not work from non-chrome code
try {
  var file = File("/etc/passwd");
  ok(false, "Did not throw on unprivileged attempt to construct a File");
} catch (e) {
  ok(true, "Threw on an unprivileged attempt to construct a File");
}

const minFileSize = 20000;
var testRanCounter = 0;
var expectedTestCount = 0;
var testSetupFinished = false;
SimpleTest.waitForExplicitFinish();

is(FileReader.EMPTY, 0, "correct EMPTY value");
is(FileReader.LOADING, 1, "correct LOADING value");
is(FileReader.DONE, 2, "correct DONE value");

// Create strings containing data we'll test with. We'll want long
// strings to ensure they span multiple buffers while loading
var testTextData = "asd b\tlah\u1234w\u00a0r";
while (testTextData.length < minFileSize) {
  testTextData = testTextData + testTextData;
}

var testASCIIData = "abcdef 123456\n";
while (testASCIIData.length < minFileSize) {
  testASCIIData = testASCIIData + testASCIIData;
}

var testBinaryData = "";
for (var i = 0; i < 256; i++) {
  testBinaryData += String.fromCharCode(i);
}
while (testBinaryData.length < minFileSize) {
  testBinaryData = testBinaryData + testBinaryData;
}

var dataurldata0 = testBinaryData.substr(0, testBinaryData.length -
					 testBinaryData.length % 3);
var dataurldata1 = testBinaryData.substr(0, testBinaryData.length - 2 -
					 testBinaryData.length % 3);
var dataurldata2 = testBinaryData.substr(0, testBinaryData.length - 1 -
					 testBinaryData.length % 3);


//Set up files for testing
var openerURL = SimpleTest.getTestFileURL("fileapi_chromeScript.js");
var opener = SpecialPowers.loadChromeScript(openerURL);
opener.addMessageListener("files.opened", onFilesOpened);
opener.sendAsyncMessage("files.open", [
  testASCIIData,
  testBinaryData,
  null,
  convertToUTF8(testTextData),
  convertToUTF16(testTextData),
  "",
  dataurldata0,
  dataurldata1,
  dataurldata2,
]);

function onFilesOpened(message) {
  let [
    asciiFile,
    binaryFile,
    nonExistingFile,
    utf8TextFile,
    utf16TextFile,
    emptyFile,
    dataUrlFile0,
    dataUrlFile1,
    dataUrlFile2,
  ] = message;

  // Test that plain reading works and fires events as expected, both
  // for text and binary reading

  var onloadHasRunText = false;
  var onloadStartHasRunText = false;
  r = new FileReader();
  is(r.readyState, FileReader.EMPTY, "correct initial text readyState");
  r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "plain reading");
  r.addEventListener("load", function() { onloadHasRunText = true }, false);
  r.addEventListener("loadstart", function() { onloadStartHasRunText = true }, false);
  r.readAsText(asciiFile);
  is(r.readyState, FileReader.LOADING, "correct loading text readyState");
  is(onloadHasRunText, false, "text loading must be async");
  is(onloadStartHasRunText, true, "text loadstart should fire sync");
  expectedTestCount++;

  var onloadHasRunBinary = false;
  var onloadStartHasRunBinary = false;
  r = new FileReader();
  is(r.readyState, FileReader.EMPTY, "correct initial binary readyState");
  r.addEventListener("load", function() { onloadHasRunBinary = true }, false);
  r.addEventListener("loadstart", function() { onloadStartHasRunBinary = true }, false);
  r.readAsBinaryString(binaryFile);
  r.onload = getLoadHandler(testBinaryData, testBinaryData.length, "binary reading");
  is(r.readyState, FileReader.LOADING, "correct loading binary readyState");
  is(onloadHasRunBinary, false, "binary loading must be async");
  is(onloadStartHasRunBinary, true, "binary loadstart should fire sync");
  expectedTestCount++;

  var onloadHasRunArrayBuffer = false;
  var onloadStartHasRunArrayBuffer = false;
  r = new FileReader();
  is(r.readyState, FileReader.EMPTY, "correct initial arrayBuffer readyState");
  r.addEventListener("load", function() { onloadHasRunArrayBuffer = true }, false);
  r.addEventListener("loadstart", function() { onloadStartHasRunArrayBuffer = true }, false);
  r.readAsArrayBuffer(binaryFile);
  r.onload = getLoadHandlerForArrayBuffer(testBinaryData, testBinaryData.length, "array buffer reading");
  is(r.readyState, FileReader.LOADING, "correct loading arrayBuffer readyState");
  is(onloadHasRunArrayBuffer, false, "arrayBuffer loading must be async");
  is(onloadStartHasRunArrayBuffer, true, "arrayBuffer loadstart should fire sync");
  expectedTestCount++;

  // Test a variety of encodings, and make sure they work properly
  r = new FileReader();
  r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "no encoding reading");
  r.readAsText(asciiFile, "");
  expectedTestCount++;

  r = new FileReader();
  r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "iso8859 reading");
  r.readAsText(asciiFile, "iso-8859-1");
  expectedTestCount++;

  r = new FileReader();
  r.onload = getLoadHandler(testTextData,
                            convertToUTF8(testTextData).length,
                            "utf8 reading");
  r.readAsText(utf8TextFile, "utf8");
  expectedTestCount++;

  r = new FileReader();
  r.readAsText(utf16TextFile, "utf-16");
  r.onload = getLoadHandler(testTextData,
                            convertToUTF16(testTextData).length,
                            "utf16 reading");
  expectedTestCount++;

  // Test get result without reading
  r = new FileReader();
  is(r.readyState, FileReader.EMPTY,
     "readyState in test reader get result without reading");
  is(r.error, null,
     "no error in test reader get result without reading");
  is(r.result, null,
     "result in test reader get result without reading");

  // Test loading an empty file works (and doesn't crash!)
  r = new FileReader();
  r.onload = getLoadHandler("", 0, "empty no encoding reading");
  r.readAsText(emptyFile, "");
  expectedTestCount++;

  r = new FileReader();
  r.onload = getLoadHandler("", 0, "empty utf8 reading");
  r.readAsText(emptyFile, "utf8");
  expectedTestCount++;

  r = new FileReader();
  r.onload = getLoadHandler("", 0, "empty utf16 reading");
  r.readAsText(emptyFile, "utf-16");
  expectedTestCount++;

  r = new FileReader();
  r.onload = getLoadHandler("", 0, "empty binary string reading");
  r.readAsBinaryString(emptyFile);
  expectedTestCount++;

  r = new FileReader();
  r.onload = getLoadHandlerForArrayBuffer("", 0, "empty array buffer reading");
  r.readAsArrayBuffer(emptyFile);
  expectedTestCount++;

  r = new FileReader();
  r.onload = getLoadHandler(convertToDataURL(""), 0, "empt binary string reading");
  r.readAsDataURL(emptyFile);
  expectedTestCount++;

  // Test reusing a FileReader to read multiple times
  r = new FileReader();
  r.onload = getLoadHandler(testASCIIData,
                            testASCIIData.length,
                            "to-be-reused reading text")
  var makeAnotherReadListener = function(event) {
    r = event.target;
    r.removeEventListener("load", makeAnotherReadListener, false);
    r.onload = getLoadHandler(testASCIIData,
                              testASCIIData.length,
                              "reused reading text");
    r.readAsText(asciiFile);
  };
  r.addEventListener("load", makeAnotherReadListener, false);
  r.readAsText(asciiFile);
  expectedTestCount += 2;

  r = new FileReader();
  r.onload = getLoadHandler(testBinaryData,
                            testBinaryData.length,
                            "to-be-reused reading binary")
  var makeAnotherReadListener2 = function(event) {
    r = event.target;
    r.removeEventListener("load", makeAnotherReadListener2, false);
    r.onload = getLoadHandler(testBinaryData,
                              testBinaryData.length,
                              "reused reading binary");
    r.readAsBinaryString(binaryFile);
  };
  r.addEventListener("load", makeAnotherReadListener2, false);
  r.readAsBinaryString(binaryFile);
  expectedTestCount += 2;

  r = new FileReader();
  r.onload = getLoadHandler(convertToDataURL(testBinaryData),
                            testBinaryData.length,
                            "to-be-reused reading data url")
  var makeAnotherReadListener3 = function(event) {
    r = event.target;
    r.removeEventListener("load", makeAnotherReadListener3, false);
    r.onload = getLoadHandler(convertToDataURL(testBinaryData),
                              testBinaryData.length,
                              "reused reading data url");
    r.readAsDataURL(binaryFile);
  };
  r.addEventListener("load", makeAnotherReadListener3, false);
  r.readAsDataURL(binaryFile);
  expectedTestCount += 2;

  r = new FileReader();
  r.onload = getLoadHandlerForArrayBuffer(testBinaryData,
                                          testBinaryData.length,
                                          "to-be-reused reading arrayBuffer")
  var makeAnotherReadListener4 = function(event) {
    r = event.target;
    r.removeEventListener("load", makeAnotherReadListener4, false);
    r.onload = getLoadHandlerForArrayBuffer(testBinaryData,
                                            testBinaryData.length,
                                            "reused reading arrayBuffer");
    r.readAsArrayBuffer(binaryFile);
  };
  r.addEventListener("load", makeAnotherReadListener4, false);
  r.readAsArrayBuffer(binaryFile);
  expectedTestCount += 2;

  // Test first reading as ArrayBuffer then read as something else
  // (BinaryString) and doesn't crash
  r = new FileReader();
  r.onload = getLoadHandlerForArrayBuffer(testBinaryData,
                                          testBinaryData.length,
                                          "to-be-reused reading arrayBuffer")
  var makeAnotherReadListener5 = function(event) {
    r = event.target;
    r.removeEventListener("load", makeAnotherReadListener5, false);
    r.onload = getLoadHandler(testBinaryData,
                              testBinaryData.length,
                              "reused reading binary string");
    r.readAsBinaryString(binaryFile);
  };
  r.addEventListener("load", makeAnotherReadListener5, false);
  r.readAsArrayBuffer(binaryFile);
  expectedTestCount += 2;

  //Test data-URI encoding on differing file sizes
  is(dataurldata0.length % 3, 0, "Want to test data with length % 3 == 0");
  r = new FileReader();
  r.onload = getLoadHandler(convertToDataURL(dataurldata0),
                            dataurldata0.length,
                            "dataurl reading, %3 = 0");
  r.readAsDataURL(dataUrlFile0);
  expectedTestCount++;

  is(dataurldata1.length % 3, 1, "Want to test data with length % 3 == 1");
  r = new FileReader();
  r.onload = getLoadHandler(convertToDataURL(dataurldata1),
                            dataurldata1.length,
                            "dataurl reading, %3 = 1");
  r.readAsDataURL(dataUrlFile1);
  expectedTestCount++;

  is(dataurldata2.length % 3, 2, "Want to test data with length % 3 == 2");
  r = new FileReader();
  r.onload = getLoadHandler(convertToDataURL(dataurldata2),
                            dataurldata2.length,
                            "dataurl reading, %3 = 2");
  r.readAsDataURL(dataUrlFile2),
  expectedTestCount++;


  // Test abort()
  var abortHasRun = false;
  var loadEndHasRun = false;
  r = new FileReader();
  r.onabort = function (event) {
    is(abortHasRun, false, "abort should only fire once");
    is(loadEndHasRun, false, "loadend shouldn't have fired yet");
    abortHasRun = true;
    is(event.target.readyState, FileReader.DONE, "should be DONE while firing onabort");
    is(event.target.error.name, "AbortError", "error set to AbortError for aborted reads");
    is(event.target.result, null, "file data should be null on aborted reads");
  }
  r.onloadend = function (event) {
    is(abortHasRun, true, "abort should fire before loadend");
    is(loadEndHasRun, false, "loadend should only fire once");
    loadEndHasRun = true;
    is(event.target.readyState, FileReader.DONE, "should be DONE while firing onabort");
    is(event.target.error.name, "AbortError", "error set to AbortError for aborted reads");
    is(event.target.result, null, "file data should be null on aborted reads");
  }
  r.onload = function() { ok(false, "load should not fire for aborted reads") };
  r.onerror = function() { ok(false, "error should not fire for aborted reads") };
  r.onprogress = function() { ok(false, "progress should not fire for aborted reads") };
  var abortThrew = false;
  try {
    r.abort();
  } catch(e) {
    abortThrew = true;
  }
  is(abortThrew, true, "abort() must throw if not loading");
  is(abortHasRun, false, "abort() is a no-op unless loading");
  r.readAsText(asciiFile);
  r.abort();
  is(abortHasRun, true, "abort should fire sync");
  is(loadEndHasRun, true, "loadend should fire sync");

  // Test calling readAsX to cause abort()
  var reuseAbortHasRun = false;
  r = new FileReader();
  r.onabort = function (event) {
    is(reuseAbortHasRun, false, "abort should only fire once");
    reuseAbortHasRun = true;
    is(event.target.readyState, FileReader.DONE, "should be DONE while firing onabort");
    is(event.target.error.name, "AbortError", "error set to AbortError for aborted reads");
    is(event.target.result, null, "file data should be null on aborted reads");
  }
  r.onload = function() { ok(false, "load should not fire for aborted reads") };
  var abortThrew = false;
  try {
    r.abort();
  } catch(e) {
    abortThrew = true;
  }
  is(abortThrew, true, "abort() must throw if not loading");
  is(reuseAbortHasRun, false, "abort() is a no-op unless loading");
  r.readAsText(asciiFile);
  r.readAsText(asciiFile);
  is(reuseAbortHasRun, true, "abort should fire sync");
  r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "reuse-as-abort reading");
  expectedTestCount++;


  // Test reading from nonexistent files
  r = new FileReader();
  var didThrow = false;
  r.onerror = function (event) {
    is(event.target.readyState, FileReader.DONE, "should be DONE while firing onerror");
    is(event.target.error.name, "NotFoundError", "error set to NotFoundError for nonexistent files");
    is(event.target.result, null, "file data should be null on aborted reads");
    testHasRun();
  };
  r.onload = function (event) {
    is(false, "nonexistent file shouldn't load! (FIXME: bug 1122788)");
    testHasRun();
  };
  try {
    r.readAsDataURL(nonExistingFile);
    expectedTestCount++;
  } catch(ex) {
    didThrow = true;
  }
  // Once this test passes, we should test that onerror gets called and
  // that the FileReader object is in the right state during that call.
  is(didThrow, false, "shouldn't throw when opening nonexistent file, should fire error instead");


  function getLoadHandler(expectedResult, expectedLength, testName) {
    return function (event) {
      is(event.target.readyState, FileReader.DONE,
         "readyState in test " + testName);
      is(event.target.error, null,
         "no error in test " + testName);
      is(event.target.result, expectedResult,
         "result in test " + testName);
      is(event.lengthComputable, true,
         "lengthComputable in test " + testName);
      is(event.loaded, expectedLength,
         "loaded in test " + testName);
      is(event.total, expectedLength,
         "total in test " + testName);
      testHasRun();
    }
  }

  function getLoadHandlerForArrayBuffer(expectedResult, expectedLength, testName) {
    return function (event) {
      is(event.target.readyState, FileReader.DONE,
         "readyState in test " + testName);
      is(event.target.error, null,
         "no error in test " +  testName);
      is(event.lengthComputable, true,
         "lengthComputable in test " + testName);
      is(event.loaded, expectedLength,
         "loaded in test " + testName);
      is(event.total, expectedLength,
         "total in test " + testName);
      is(event.target.result.byteLength, expectedLength,
         "array buffer size in test " + testName);
      var u8v = new Uint8Array(event.target.result);
      is(String.fromCharCode.apply(String, u8v), expectedResult,
         "array buffer contents in test " + testName);
      u8v = null;
      SpecialPowers.gc();
      is(event.target.result.byteLength, expectedLength,
         "array buffer size after gc in test " + testName);
      u8v = new Uint8Array(event.target.result);
      is(String.fromCharCode.apply(String, u8v), expectedResult,
         "array buffer contents after gc in test " + testName);
      testHasRun();
    }
  }

  function testHasRun() {
    //alert(testRanCounter);
    ++testRanCounter;
    if (testRanCounter == expectedTestCount) {
      is(testSetupFinished, true, "test setup should have finished; check for exceptions");
      is(onloadHasRunText, true, "onload text should have fired by now");
      is(onloadHasRunBinary, true, "onload binary should have fired by now");
      opener.destroy();
      SimpleTest.finish();
    }
  }

  testSetupFinished = true;
}

function convertToUTF16(s) {
  res = "";
  for (var i = 0; i < s.length; ++i) {
    c = s.charCodeAt(i);
    res += String.fromCharCode(c & 255, c >>> 8);
  }
  return res;
}

function convertToUTF8(s) {
  return unescape(encodeURIComponent(s));
}

function convertToDataURL(s) {
  return "data:application/octet-stream;base64," + btoa(s);
}

</script>
</pre>
</body> </html>