Add POST support to XHR as well as .status and statusText support

Also fixed all the XHR tests to actually run and work
I learned from elliot that the it function has to
take a "done" argument to be treated asynchronously.

My utf8 conversion is a hack, but we deleted
window.TextEncoder (it was a module) when we forked.
We could resurrect TextEncoder (and probably should)
but I've left that for a separate change.

I also augmented sky_server to have a special /echo_post
handler so we can test POST requests.

R=esprehn@chromium.org
BUG=

Committed: https://chromium.googlesource.com/external/mojo/+/5ef81d249b841f44c9593ffb0158d64c7e0febfc

Review URL: https://codereview.chromium.org/810523002
diff --git a/sky/framework/xmlhttprequest.sky b/sky/framework/xmlhttprequest.sky
index 897ad97..eb8aa9e 100644
--- a/sky/framework/xmlhttprequest.sky
+++ b/sky/framework/xmlhttprequest.sky
@@ -21,11 +21,24 @@
   }
 }
 
+// Somewhat hacky, but works.
+function stringToUTF8Buffer(string) {
+    var string = unescape(encodeURIComponent(string));
+    var charList = string.split('');
+    var uintArray = [];
+    for (var i = 0; i < charList.length; i++) {
+        uintArray.push(charList[i].charCodeAt(0));
+    }
+    return new Uint8Array(uintArray);
+}
+
 // https://xhr.spec.whatwg.org
 class XMLHttpRequest {
   constructor() {
     this[kPrivate] = new Private;
     this.responseType = ''; // Only text and arraybuffer support for now.
+    this.status = null;
+    this.statusText = null;
   }
 
   onload() {
@@ -56,7 +69,7 @@
 
   open(method, url) {
     var request = new loader.URLRequest();
-    request.url = new URL(url, document.URL);
+    request.url = String(new URL(url, document.URL));
     request.method = method;
     request.auto_follow_redirects = true;
 
@@ -69,8 +82,29 @@
     this[kPrivate].headers.set(header, value);
   }
 
-  send() {
+  send(body) {
     var priv = this[kPrivate];
+    // Handle the body before the headers as it can affect Content-Type.
+    if (body) {
+      var bodyAsBufferView = null;
+      if (typeof(body) === "string") {
+        this.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");
+        bodyAsBufferView = stringToUTF8Buffer(body);
+      } else {
+        bodyAsBufferView = new Uint8Array(body);
+      }
+      var dataPipe = new core.createDataPipe();
+      // FIXME: body is currently assumed to be an ArrayBuffer.
+      var writeResult = core.writeData(dataPipe.producerHandle,
+        bodyAsBufferView, core.WRITE_DATA_FLAG_ALL_OR_NONE);
+      core.close(dataPipe.producerHandle);
+      // FIXME: Much better error handling needed.
+      console.assert(writeResult.result === core.RESULT_OK);
+      console.assert(writeResult.numBytes === body.length);
+      // 'body' is actually an array of body segments.
+      priv.request.body = [dataPipe.consumerHandle];
+    }
+
     var requestHeaders = [];
     priv.headers.forEach(function(value, key) {
       requestHeaders.push(key + ': ' + value);
@@ -85,14 +119,21 @@
     var self = this;
     outstandingRequests.add(this);
     priv.loader.start(priv.request).then(function(result) {
+      self.status = result.response.status_code;
+      self.statusText = result.response.status_line;
+      if (result.response.error)
+        throw new Error(result.response.error.description);
       return core.drainData(result.response.body).then(function(result) {
         outstandingRequests.delete(self);
         priv.responseArrayBuffer = result.buffer;
-        // FIXME: Catch exceptions during onload so they don't trip onerror.
-        self.onload();
+        // Use a setTimeout to avoid exceptions in onload tripping onerror.
+        window.setTimeout(function() {
+          self.onload();
+        });
       });
     }).catch(function(error) {
       outstandingRequests.delete(self);
+      // Technically this should throw a ProgressEvent.
       self.onerror(error);
     });
   }
diff --git a/sky/tests/framework/xmlhttprequest/empty-responseType.sky b/sky/tests/framework/xmlhttprequest/empty-responseType.sky
index 19a132b..f97e254 100644
--- a/sky/tests/framework/xmlhttprequest/empty-responseType.sky
+++ b/sky/tests/framework/xmlhttprequest/empty-responseType.sky
@@ -4,9 +4,7 @@
 <import src="/sky/framework/xmlhttprequest.sky" as="XMLHttpRequest" />
 <script>
 describe("xmlhttprequest.responseType", function() {
-  this.enableTimeouts(false);
-
-  it("should default to text when empty", function() {
+  it("should default to text when empty", function(done) {
     var xhr = new XMLHttpRequest();
     assert.equal(xhr.responseType, "");
     xhr.responseType = 'foo';
diff --git a/sky/tests/framework/xmlhttprequest/responseType.sky b/sky/tests/framework/xmlhttprequest/responseType.sky
index 5153093..f220009 100644
--- a/sky/tests/framework/xmlhttprequest/responseType.sky
+++ b/sky/tests/framework/xmlhttprequest/responseType.sky
@@ -4,12 +4,11 @@
 <import src="/sky/framework/xmlhttprequest.sky" as="XMLHttpRequest" />
 <script>
 describe("xmlhttprequest.responseType", function() {
-  this.enableTimeouts(false);
-
-  it("should support arraybuffer", function() {
+  it("should support arraybuffer", function(done) {
     var xhr = new XMLHttpRequest();
+    xhr.responseType = 'arraybuffer';
     xhr.onload = function() {
-      assert.typeOf(this.response, "arraybuffer", "Response is an arraybuffer\n");
+      assert.instanceOf(xhr.response, ArrayBuffer, "Response is an ArrayBuffer\n");
       done();
     };
     xhr.open("GET", "resources/pass.txt");
diff --git a/sky/tests/framework/xmlhttprequest/unicode-post-expected.txt b/sky/tests/framework/xmlhttprequest/unicode-post-expected.txt
new file mode 100644
index 0000000..64c865d
--- /dev/null
+++ b/sky/tests/framework/xmlhttprequest/unicode-post-expected.txt
@@ -0,0 +1,5 @@
+Running 1 tests
+ok 1 XMLHttpRequest should be able to post non-ascii
+1 tests
+1 pass
+0 fail
diff --git a/sky/tests/framework/xmlhttprequest/unicode-post.sky b/sky/tests/framework/xmlhttprequest/unicode-post.sky
new file mode 100644
index 0000000..e785741
--- /dev/null
+++ b/sky/tests/framework/xmlhttprequest/unicode-post.sky
@@ -0,0 +1,22 @@
+<html>
+<import src="/sky/tests/resources/chai.sky" />
+<import src="/sky/tests/resources/mocha.sky" />
+<import src="/sky/framework/xmlhttprequest.sky" as="XMLHttpRequest" />
+<script>
+describe('XMLHttpRequest', function() {
+  it('should be able to post non-ascii', function(done) {
+    // example utf8, #114, "I can eat glass" in arabic.
+    // http://www.columbia.edu/~kermit/utf8.html
+    var utf8_text = "أنا قادر على أكل الزجاج و هذا لا يؤلمني.";
+
+    var xhr = new XMLHttpRequest();
+    xhr.onload = function() {
+      assert.equal(this.responseText, utf8_text);
+      done();
+    };
+    xhr.open("GET", "/echo_post");
+    xhr.send(utf8_text);
+  });
+});
+</script>
+</html>
diff --git a/sky/tests/framework/xmlhttprequest/xhr-does-not-exist-expected.txt b/sky/tests/framework/xmlhttprequest/xhr-does-not-exist-expected.txt
new file mode 100644
index 0000000..fd4506d
--- /dev/null
+++ b/sky/tests/framework/xmlhttprequest/xhr-does-not-exist-expected.txt
@@ -0,0 +1,5 @@
+Running 1 tests
+ok 1 xmlhttprequest should call onerror when endpoint does not exist
+1 tests
+1 pass
+0 fail
diff --git a/sky/tests/framework/xmlhttprequest/xhr-does-not-exist.sky b/sky/tests/framework/xmlhttprequest/xhr-does-not-exist.sky
new file mode 100644
index 0000000..e88c4c9
--- /dev/null
+++ b/sky/tests/framework/xmlhttprequest/xhr-does-not-exist.sky
@@ -0,0 +1,26 @@
+<sky>
+<import src="/sky/tests/resources/chai.sky" />
+<import src="/sky/tests/resources/mocha.sky" />
+<import src="/sky/framework/xmlhttprequest.sky" as="XMLHttpRequest" />
+<script>
+describe("xmlhttprequest", function() {
+  it("should call onerror when endpoint does not exist", function(done) {
+    var xhr = new XMLHttpRequest();
+    xhr.open("GET", "does_not_exist.html");
+    xhr.onerror = function() {
+      assert.fail("onload", "onerror", "onerror should not be called.");
+      done();
+    }
+    xhr.onload = function() {
+      // Missing files are application-level errors, not network errors
+      // so onload fires, not onerror.
+      assert.equal(xhr.status, 404);
+      assert.equal(xhr.statusText, "HTTP/1.1 404 Not Found",
+          "status text should also be 404");
+      done();
+    }
+    xhr.send();
+  });
+});
+</script>
+</sky>
diff --git a/sky/tests/framework/xmlhttprequest/xhr-relative.sky b/sky/tests/framework/xmlhttprequest/xhr-relative.sky
index 19a9034..632810f 100644
--- a/sky/tests/framework/xmlhttprequest/xhr-relative.sky
+++ b/sky/tests/framework/xmlhttprequest/xhr-relative.sky
@@ -4,8 +4,6 @@
 <import src="/sky/framework/xmlhttprequest.sky" as="XMLHttpRequest" />
 <script>
 describe('XMLHttpRequest', function() {
-  this.enableTimeouts(false);
-
   it('should be able to fetch relative urls', function(done) {
 
     var xhr = new XMLHttpRequest();
diff --git a/sky/tests/framework/xmlhttprequest/xhr.sky b/sky/tests/framework/xmlhttprequest/xhr.sky
index c19f36d..d730963 100644
--- a/sky/tests/framework/xmlhttprequest/xhr.sky
+++ b/sky/tests/framework/xmlhttprequest/xhr.sky
@@ -4,8 +4,6 @@
 <import src="/sky/framework/xmlhttprequest.sky" as="XMLHttpRequest" />
 <script>
 describe('XMLHttpRequest', function() {
-  this.enableTimeouts(false);
-
   it('should be able to fetch text files', function(done) {
 
     var xhr = new XMLHttpRequest();
diff --git a/sky/tools/skygo/sky_server.go b/sky/tools/skygo/sky_server.go
index c69fdd5..e851487 100644
--- a/sky/tools/skygo/sky_server.go
+++ b/sky/tools/skygo/sky_server.go
@@ -6,6 +6,7 @@
 
 import (
     "flag"
+    "io/ioutil"
     "net/http"
     "path"
     "strings"
@@ -38,6 +39,11 @@
     genRoot := path.Join(root, "out", *configuration, "gen")
 
     http.Handle("/", skyHandler(root))
+    http.HandleFunc("/echo_post", func(w http.ResponseWriter, r *http.Request) {
+        defer r.Body.Close()
+        body, _ := ioutil.ReadAll(r.Body)
+        w.Write(body)
+    })
     http.Handle("/mojo/public/", http.StripPrefix("/mojo/public/", skyHandler(path.Join(genRoot, "mojo", "public"))))
     http.Handle("/mojo/services/", http.StripPrefix("/mojo/services/", skyHandler(path.Join(genRoot, "mojo", "services"))))
     http.Handle("/sky/services/", http.StripPrefix("/sky/services/", skyHandler(path.Join(genRoot, "sky", "services"))))
diff --git a/sky/tools/skygo/sky_server.sha1 b/sky/tools/skygo/sky_server.sha1
index 38a08c7..8b7306e 100644
--- a/sky/tools/skygo/sky_server.sha1
+++ b/sky/tools/skygo/sky_server.sha1
@@ -1 +1 @@
-7b6d0d60949ad71a35a88430b2eb7d70fe7334b6
\ No newline at end of file
+f6b808791e8ab0290cb18bc8b444159074c395ae
\ No newline at end of file