Bug 687087 part 3. Implement XHR.responseType="moz-chunked-text" and XHR.responseType="moz-chunked-arraybuffer". r=smaug on code changes, rs=smaug on tests.
authorJonas Sicking <jonas@sicking.cc>
Fri, 23 Sep 2011 18:57:36 -0700
changeset 78801 b2fb48fdb1e73f6d7d5511a0efea52682b43033d
parent 78800 192254be8f97f33cb59f9783d559956fe371edaa
child 78802 e9f4b05cfbaaa7f474e2d212e2dcae0f33557226
push id78
push userclegnitto@mozilla.com
push dateFri, 16 Dec 2011 17:32:24 +0000
treeherdermozilla-release@79d24e644fdd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug, smaug
bugs687087
milestone9.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 687087 part 3. Implement XHR.responseType="moz-chunked-text" and XHR.responseType="moz-chunked-arraybuffer". r=smaug on code changes, rs=smaug on tests.
content/base/src/nsXMLHttpRequest.cpp
content/base/src/nsXMLHttpRequest.h
content/base/test/Makefile.in
content/base/test/progressserver.sjs
content/base/test/test_XHR.html
content/base/test/test_xhr_progressevents.html
--- a/content/base/src/nsXMLHttpRequest.cpp
+++ b/content/base/src/nsXMLHttpRequest.cpp
@@ -423,16 +423,17 @@ nsXMLHttpRequest::nsXMLHttpRequest()
     mRequestObserver(nsnull), mState(XML_HTTP_REQUEST_UNSENT),
     mUploadTransferred(0), mUploadTotal(0), mUploadComplete(PR_TRUE),
     mProgressSinceLastProgressEvent(PR_FALSE),
     mUploadProgress(0), mUploadProgressMax(0),
     mErrorLoad(PR_FALSE), mTimerIsActive(PR_FALSE),
     mProgressEventWasDelayed(PR_FALSE),
     mLoadLengthComputable(PR_FALSE), mLoadTotal(0),
     mFirstStartRequestSeen(PR_FALSE),
+    mInLoadProgressEvent(PR_FALSE),
     mResultJSON(JSVAL_VOID),
     mResultArrayBuffer(nsnull)
 {
   nsLayoutStatics::AddRef();
 }
 
 nsXMLHttpRequest::~nsXMLHttpRequest()
 {
@@ -718,17 +719,18 @@ nsXMLHttpRequest::GetResponseXML(nsIDOMD
 nsresult
 nsXMLHttpRequest::DetectCharset()
 {
   mResponseCharset.Truncate();
   mDecoder = nsnull;
 
   if (mResponseType != XML_HTTP_RESPONSE_TYPE_DEFAULT &&
       mResponseType != XML_HTTP_RESPONSE_TYPE_TEXT &&
-      mResponseType != XML_HTTP_RESPONSE_TYPE_JSON) {
+      mResponseType != XML_HTTP_RESPONSE_TYPE_JSON &&
+      mResponseType != XML_HTTP_RESPONSE_TYPE_CHUNKED_TEXT) {
     return NS_OK;
   }
 
   nsCOMPtr<nsIChannel> channel = do_QueryInterface(mReadRequest);
   if (!channel) {
     channel = mChannel;
   }
 
@@ -814,20 +816,27 @@ nsXMLHttpRequest::AppendToResponseText(c
 }
 
 /* readonly attribute AString responseText; */
 NS_IMETHODIMP nsXMLHttpRequest::GetResponseText(nsAString& aResponseText)
 {
   aResponseText.Truncate();
 
   if (mResponseType != XML_HTTP_RESPONSE_TYPE_DEFAULT &&
-      mResponseType != XML_HTTP_RESPONSE_TYPE_TEXT) {
+      mResponseType != XML_HTTP_RESPONSE_TYPE_TEXT &&
+      mResponseType != XML_HTTP_RESPONSE_TYPE_CHUNKED_TEXT) {
     return NS_ERROR_DOM_INVALID_STATE_ERR;
   }
 
+  if (mResponseType == XML_HTTP_RESPONSE_TYPE_CHUNKED_TEXT &&
+      !mInLoadProgressEvent) {
+    aResponseText.SetIsVoid(PR_TRUE);
+    return NS_OK;
+  }
+
   if (!(mState & (XML_HTTP_REQUEST_DONE | XML_HTTP_REQUEST_LOADING))) {
     return NS_OK;
   }
 
   // We only decode text lazily if we're also parsing to a doc.
   // Also, if we've decoded all current data already, then no need to decode
   // more.
   if (!mResponseXML ||
@@ -928,16 +937,22 @@ NS_IMETHODIMP nsXMLHttpRequest::GetRespo
     aResponseType.AssignLiteral("document");
     break;
   case XML_HTTP_RESPONSE_TYPE_TEXT:
     aResponseType.AssignLiteral("text");
     break;
   case XML_HTTP_RESPONSE_TYPE_JSON:
     aResponseType.AssignLiteral("moz-json");
     break;
+  case XML_HTTP_RESPONSE_TYPE_CHUNKED_TEXT:
+    aResponseType.AssignLiteral("moz-chunked-text");
+    break;
+  case XML_HTTP_RESPONSE_TYPE_CHUNKED_ARRAYBUFFER:
+    aResponseType.AssignLiteral("moz-chunked-arraybuffer");
+    break;
   default:
     NS_ERROR("Should not happen");
   }
 
   return NS_OK;
 }
 
 /* attribute AString responseType; */
@@ -957,16 +972,26 @@ NS_IMETHODIMP nsXMLHttpRequest::SetRespo
   } else if (aResponseType.EqualsLiteral("blob")) {
     mResponseType = XML_HTTP_RESPONSE_TYPE_BLOB;
   } else if (aResponseType.EqualsLiteral("document")) {
     mResponseType = XML_HTTP_RESPONSE_TYPE_DOCUMENT;
   } else if (aResponseType.EqualsLiteral("text")) {
     mResponseType = XML_HTTP_RESPONSE_TYPE_TEXT;
   } else if (aResponseType.EqualsLiteral("moz-json")) {
     mResponseType = XML_HTTP_RESPONSE_TYPE_JSON;
+  } else if (aResponseType.EqualsLiteral("moz-chunked-text")) {
+    if (!(mState & XML_HTTP_REQUEST_ASYNC)) {
+      return NS_ERROR_DOM_INVALID_STATE_ERR;
+    }
+    mResponseType = XML_HTTP_RESPONSE_TYPE_CHUNKED_TEXT;
+  } else if (aResponseType.EqualsLiteral("moz-chunked-arraybuffer")) {
+    if (!(mState & XML_HTTP_REQUEST_ASYNC)) {
+      return NS_ERROR_DOM_INVALID_STATE_ERR;
+    }
+    mResponseType = XML_HTTP_RESPONSE_TYPE_CHUNKED_ARRAYBUFFER;
   }
   // If the given value is not the empty string, "arraybuffer",
   // "blob", "document", or "text" terminate these steps.
 
   // If the state is OPENED, SetCacheAsFile would have no effect here
   // because the channel hasn't initialized the cache entry yet.
   // SetCacheAsFile will be called from OnStartRequest.
   // If the state is HEADERS_RECEIVED, however, we need to call
@@ -984,30 +1009,39 @@ NS_IMETHODIMP nsXMLHttpRequest::SetRespo
 /* readonly attribute jsval response; */
 NS_IMETHODIMP nsXMLHttpRequest::GetResponse(JSContext *aCx, jsval *aResult)
 {
   nsresult rv = NS_OK;
 
   switch (mResponseType) {
   case XML_HTTP_RESPONSE_TYPE_DEFAULT:
   case XML_HTTP_RESPONSE_TYPE_TEXT:
+  case XML_HTTP_RESPONSE_TYPE_CHUNKED_TEXT:
     {
       nsString str;
       rv = GetResponseText(str);
       if (NS_FAILED(rv)) return rv;
-      nsStringBuffer* buf;
-      *aResult = XPCStringConvert::ReadableToJSVal(aCx, str, &buf);
-      if (buf) {
-        str.ForgetSharedBuffer();
+      if (str.IsVoid()) {
+        *aResult = JSVAL_NULL;
+      } else {
+        nsStringBuffer* buf;
+        *aResult = XPCStringConvert::ReadableToJSVal(aCx, str, &buf);
+        if (buf) {
+          str.ForgetSharedBuffer();
+        }
       }
     }
     break;
 
   case XML_HTTP_RESPONSE_TYPE_ARRAYBUFFER:
-    if (mState & XML_HTTP_REQUEST_DONE) {
+  case XML_HTTP_RESPONSE_TYPE_CHUNKED_ARRAYBUFFER:
+    if ((mResponseType == XML_HTTP_RESPONSE_TYPE_ARRAYBUFFER &&
+         mState & XML_HTTP_REQUEST_DONE) ||
+        (mResponseType == XML_HTTP_RESPONSE_TYPE_CHUNKED_ARRAYBUFFER &&
+         mInLoadProgressEvent)) {
       if (!mResultArrayBuffer) {
          rv = CreateResponseArrayBuffer(aCx);
          NS_ENSURE_SUCCESS(rv, rv);
       }
       *aResult = OBJECT_TO_JSVAL(mResultArrayBuffer);
     } else {
       *aResult = JSVAL_NULL;
     }
@@ -1599,27 +1633,28 @@ nsXMLHttpRequest::StreamReaderFunc(nsIIn
       xmlHttpRequest->mResponseBlob) {
     *writeCount = count;
     return NS_OK;
   }
 
   if ((xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_DEFAULT &&
        xmlHttpRequest->mResponseXML) ||
       xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_ARRAYBUFFER ||
-      xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_BLOB) {
+      xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_BLOB ||
+      xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_CHUNKED_ARRAYBUFFER) {
     // Copy for our own use
     PRUint32 previousLength = xmlHttpRequest->mResponseBody.Length();
     xmlHttpRequest->mResponseBody.Append(fromRawSegment,count);
     if (count > 0 && xmlHttpRequest->mResponseBody.Length() == previousLength) {
       return NS_ERROR_OUT_OF_MEMORY;
     }
-  }
-  else if (xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_DEFAULT ||
-           xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_TEXT ||
-           xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_JSON) {
+  } else if (xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_DEFAULT ||
+             xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_TEXT ||
+             xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_JSON ||
+             xmlHttpRequest->mResponseType == XML_HTTP_RESPONSE_TYPE_CHUNKED_TEXT) {
     NS_ASSERTION(!xmlHttpRequest->mResponseXML,
                  "We shouldn't be parsing a doc here");
     xmlHttpRequest->AppendToResponseText(fromRawSegment, count);
   }
 
   nsresult rv = NS_OK;
 
   if (xmlHttpRequest->mState & XML_HTTP_REQUEST_PARSEBODY) {
@@ -2986,19 +3021,27 @@ nsXMLHttpRequest::MaybeDispatchProgressE
                             mUploadTotal, mUploadProgress,
                             mUploadProgressMax);
     }
   } else {
     if (aFinalProgress) {
       mLoadTotal = mLoadTransferred;
       mLoadLengthComputable = PR_TRUE;
     }
+    mInLoadProgressEvent = PR_TRUE;
     DispatchProgressEvent(this, NS_LITERAL_STRING(PROGRESS_STR),
                           PR_TRUE, mLoadLengthComputable, mLoadTransferred,
                           mLoadTotal, mLoadTransferred, mLoadTotal);
+    mInLoadProgressEvent = PR_FALSE;
+    if (mResponseType == XML_HTTP_RESPONSE_TYPE_CHUNKED_TEXT ||
+        mResponseType == XML_HTTP_RESPONSE_TYPE_CHUNKED_ARRAYBUFFER) {
+      mResponseBody.Truncate();
+      mResponseText.Truncate();
+      mResultArrayBuffer = nsnull;
+    }
   }
 
   mProgressSinceLastProgressEvent = PR_FALSE;
 }
 
 NS_IMETHODIMP
 nsXMLHttpRequest::OnProgress(nsIRequest *aRequest, nsISupports *aContext, PRUint64 aProgress, PRUint64 aProgressMax)
 {
--- a/content/base/src/nsXMLHttpRequest.h
+++ b/content/base/src/nsXMLHttpRequest.h
@@ -300,17 +300,19 @@ protected:
   nsCString mResponseCharset;
 
   enum {
     XML_HTTP_RESPONSE_TYPE_DEFAULT,
     XML_HTTP_RESPONSE_TYPE_ARRAYBUFFER,
     XML_HTTP_RESPONSE_TYPE_BLOB,
     XML_HTTP_RESPONSE_TYPE_DOCUMENT,
     XML_HTTP_RESPONSE_TYPE_TEXT,
-    XML_HTTP_RESPONSE_TYPE_JSON
+    XML_HTTP_RESPONSE_TYPE_JSON,
+    XML_HTTP_RESPONSE_TYPE_CHUNKED_TEXT,
+    XML_HTTP_RESPONSE_TYPE_CHUNKED_ARRAYBUFFER
   } mResponseType;
 
   nsCOMPtr<nsIDOMBlob> mResponseBlob;
 
   nsCString mOverrideMimeType;
 
   /**
    * The notification callbacks the channel had when Send() was
@@ -345,16 +347,17 @@ protected:
   PRPackedBool mTimerIsActive;
   PRPackedBool mProgressEventWasDelayed;
   PRPackedBool mLoadLengthComputable;
   PRUint64 mLoadTotal; // 0 if not known.
   PRUint64 mLoadTransferred;
   nsCOMPtr<nsITimer> mProgressNotifier;
 
   PRPackedBool mFirstStartRequestSeen;
+  PRPackedBool mInLoadProgressEvent;
   
   nsCOMPtr<nsIAsyncVerifyRedirectCallback> mRedirectCallback;
   nsCOMPtr<nsIChannel> mNewRedirectChannel;
   
   jsval mResultJSON;
   JSObject* mResultArrayBuffer;
 
   void ResetResponse();
--- a/content/base/test/Makefile.in
+++ b/content/base/test/Makefile.in
@@ -494,16 +494,18 @@ include $(topsrcdir)/config/rules.mk
 		badMessageEvent.eventsource \
 		badMessageEvent.eventsource^headers^ \
 		forRemoval.resource \
 		forRemoval.resource^headers^ \
 		accesscontrol.resource \
 		accesscontrol.resource^headers^ \
 		invalid_accesscontrol.resource \
 		invalid_accesscontrol.resource^headers^ \
+		test_xhr_progressevents.html \
+		progressserver.sjs \
 		somedatas.resource \
 		somedatas.resource^headers^ \
 		delayedServerEvents.sjs \
 		test_bug664916.html \
 		test_bug666604.html \
 		test_bug675121.html \
 		file_bug675121.sjs \
 		test_bug654352.html \
new file mode 100644
--- /dev/null
+++ b/content/base/test/progressserver.sjs
@@ -0,0 +1,51 @@
+const CC = Components.Constructor;
+const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
+                             "nsIBinaryInputStream",
+                             "setInputStream");
+
+function setReq(req) {
+  setObjectState("content/base/test/progressserver", req);
+}
+
+function getReq() {
+  var req;
+  getObjectState("content/base/test/progressserver", function(v) {
+    req = v;
+  });
+  return req;
+}
+
+function handleRequest(request, response)
+{
+  var pairs = request.queryString.split('&');
+  var command = pairs.shift();
+
+  var bodyStream = new BinaryInputStream(request.bodyInputStream);
+  var body = "";
+  var bodyAvail;
+  while ((bodyAvail = bodyStream.available()) > 0)
+    body += String.fromCharCode.apply(null, bodyStream.readByteArray(bodyAvail));
+
+  if (command == "open") {
+    response.processAsync();
+    setReq(response);
+
+    response.setHeader("Cache-Control", "no-cache", false);
+    pairs.forEach(function (val) {
+      var [name, value] = val.split('=');
+      response.setHeader(name, unescape(value), false);
+    });
+    response.write(body);
+    return;
+  }
+
+  if (command == "send") {
+    getReq().write(body);
+  }
+  else if (command == "close") {
+    getReq().finish();
+    setReq(null);
+  }
+  response.setHeader("Content-Type", "text/plain");
+  response.write("ok");
+}
--- a/content/base/test/test_XHR.html
+++ b/content/base/test/test_XHR.html
@@ -9,51 +9,16 @@
 <p id="display"></p>
 <div id="content" style="display: none">
   
 </div>
 <pre id="test">
 <script class="testbody" type="text/javascript">
 SimpleTest.waitForExplicitFinish();
 
-// test receiving as JSON
-function testJSON(aJsonStr, invalid) {
-  var errorThrown = false;
-  var anotherErrorThrown = false;
-  var xhr = new XMLHttpRequest();
-  
-  var didthrow = false;
-  try { xhr.responseType = 'moz-json'; } catch (e) { didthrow = true; }
-  ok(didthrow, 
-     "should have thrown when setting responseType to moz-json before open");
-
-  xhr.open("POST", 'responseIdentical.sjs', false);
-  xhr.responseType = 'moz-json';
-  xhr.send(aJsonStr);
-
-  if (!invalid) {
-    is(JSON.stringify(xhr.response), aJsonStr);
-    is(xhr.response, xhr.response, "returning the same object on each access");
-  }
-  else {
-    var didThrow = false;
-    try { xhr.response } catch(ex) { didThrow = true; }
-    ok(didThrow, "accessing response should throw");
-
-    didThrow = false;
-    try { xhr.response } catch(ex) { didThrow = true; }
-    ok(didThrow, "accessing response should throw");
-  } 
-}
-
-var jsonStr = '{"title":"aBook","author":"john"}';
-testJSON(jsonStr, false);
-var invalidJson = '{ "abc": }'
-testJSON(invalidJson, true);
-
 var path = "/tests/content/base/test/";
 
 var passFiles = [['file_XHR_pass1.xml', 'GET'],
                  ['file_XHR_pass2.txt', 'GET'],
                  ['file_XHR_pass3.txt', 'GET'],
                  ];
 
 var failFiles = [['//example.com' + path + 'file_XHR_pass1.xml', 'GET'],
@@ -104,28 +69,35 @@ function checkResponseTextAccessThrows(x
   try { xhr.responseText } catch (e) { didthrow = true; }
   ok(didthrow, "should have thrown when accessing responseText");
 }
 function checkResponseXMLAccessThrows(xhr) {
   var didthrow = false;
   try { xhr.responseXML } catch (e) { didthrow = true; }
   ok(didthrow, "should have thrown when accessing responseXML");
 }
-function checkSetResponseTypeThrows(xhr) {
+function checkResponseAccessThrows(xhr) {
   var didthrow = false;
-  try { xhr.responseType = 'document'; } catch (e) { didthrow = true; }
-  ok(didthrow, "should have thrown when accessing responseType");
+  try { xhr.response } catch (e) { didthrow = true; }
+  ok(didthrow, "should have thrown when accessing response");
+}
+function checkSetResponseTypeThrows(xhr, type) {
+  var didthrow = false;
+  try { xhr.responseType = type; } catch (e) { didthrow = true; }
+  ok(didthrow, "should have thrown when setting responseType");
 }
 
 xhr = new XMLHttpRequest();
-checkSetResponseTypeThrows(xhr);
+checkSetResponseTypeThrows(xhr, "document");
 xhr.open("GET", 'file_XHR_pass1.xml', false); 
+checkSetResponseTypeThrows(xhr, "moz-chunked-text");
+checkSetResponseTypeThrows(xhr, "moz-chunked-arraybuffer");
 xhr.responseType = 'document';
 xhr.send(null);
-checkSetResponseTypeThrows(xhr);
+checkSetResponseTypeThrows(xhr, "document");
 is(xhr.status, 200, "wrong status");
 checkResponseTextAccessThrows(xhr);
 is((new XMLSerializer()).serializeToString(xhr.response.documentElement),
    "<res>hello</res>",
    "wrong response");
 
 // test response (responseType='text')
 xhr = new XMLHttpRequest();
@@ -173,26 +145,40 @@ xhr.open("GET", 'file_XHR_binary1.bin', 
 xhr.responseType = 'arraybuffer';
 xhr.send(null)
 is(xhr.status, 200, "wrong status");
 checkResponseTextAccessThrows(xhr);
 checkResponseXMLAccessThrows(xhr);
 ab = xhr.response;
 ok(ab != null, "should have a non-null arraybuffer");
 arraybuffer_equals_to(ab, "\xaa\xee\0\x03\xff\xff\xff\xff\xbb\xbb\xbb\xbb");
+is(xhr.response, xhr.response, "returns the same ArrayBuffer");
 
-// test array buffer GetResult returns the same object
-xhr = new XMLHttpRequest();
-xhr.open("GET", 'file_XHR_binary1.bin', false); 
-xhr.responseType = 'arraybuffer';
-xhr.send(null)
+// test response (responseType='moz-json')
+var xhr = new XMLHttpRequest();
+xhr.open("POST", 'responseIdentical.sjs', false);
+xhr.responseType = 'moz-json';
+jsonObjStr = JSON.stringify({title: "aBook", author: "john"});
+xhr.send(jsonObjStr);
 is(xhr.status, 200, "wrong status");
 checkResponseTextAccessThrows(xhr);
 checkResponseXMLAccessThrows(xhr);
-is(xhr.response, xhr.response, "returns the same ArrayBuffer");
+is(JSON.stringify(xhr.response), jsonObjStr, "correct result");
+is(xhr.response, xhr.response, "returning the same object on each access");
+
+// with invalid json
+var xhr = new XMLHttpRequest();
+xhr.open("POST", 'responseIdentical.sjs', false);
+xhr.responseType = 'moz-json';
+xhr.send("{");
+is(xhr.status, 200, "wrong status");
+checkResponseTextAccessThrows(xhr);
+checkResponseXMLAccessThrows(xhr);
+checkResponseAccessThrows(xhr);
+checkResponseAccessThrows(xhr); // Check twice to ensure that we still throw
 
 // test response (responseType='blob')
 var onloadCount = 0;
 function checkOnloadCount() {
   if (++onloadCount >= 2) SimpleTest.finish();
 };
 
 // with a simple text file
new file mode 100644
--- /dev/null
+++ b/content/base/test/test_xhr_progressevents.html
@@ -0,0 +1,282 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for XMLHttpRequest Progress Events</title>
+  <script type="text/javascript" src="/MochiKit/packed.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>        
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onload="gen.next();">
+<pre id=l></pre>
+<script type="application/javascript;version=1.7">
+SimpleTest.waitForExplicitFinish();
+
+var gen = runTests();
+
+function log(s) {
+  //document.getElementById("l").textContent += s + "\n";
+}
+
+function getEvent(e) {
+  log("got event: " + e.type + " (" + e.target.readyState + ")");
+  gen.send(e);
+}
+
+function startsWith(a, b) {
+  return a.substr(0, b.length) === b;
+}
+
+function updateProgress(e, data, testName) {
+  var test = " while running " + testName;
+  is(e.type, "progress", "event type" + test);
+  
+  let response;
+  if (data.nodata) {
+    is(e.target.response, null, "response should be null" + test);
+    response = null;
+  }
+  else if (data.text) {
+    is(typeof e.target.response, "string", "response should be a string" + test);
+    response = e.target.response;
+  }
+  else {
+    ok(e.target.response instanceof ArrayBuffer, "response should be a ArrayBuffer" + test);
+    response = bufferToString(e.target.response);
+  }
+
+  if (!data.nodata && !data.encoded) {
+    if (!data.chunked) {
+      is(e.loaded, response.length, "event.loaded matches response size" + test);
+    }
+    else {
+      is(e.loaded - data.receivedBytes, response.length,
+         "event.loaded grew by response size" + test);
+    }
+  }
+  ok(e.loaded > data.receivedBytes, "event.loaded increased" + test);
+  ok(e.loaded - data.receivedBytes <= data.pendingBytes,
+     "event.loaded didn't increase too much" + test);
+
+  if (!data.nodata) {
+    var newData;
+    ok(startsWith(response, data.receivedResult),
+       "response strictly grew" + test);
+    newData = response.substr(data.receivedResult.length);
+  
+    if (!data.encoded) {
+      ok(newData.length > 0, "sanity check for progress" + test);
+    }
+    ok(startsWith(data.pendingResult, newData), "new data matches expected" + test);
+  }
+
+  is(e.lengthComputable, "total" in data, "lengthComputable" + test);
+  if ("total" in data) {
+    is(e.total, data.total, "total" + test);
+  }
+
+  if (!data.nodata) {
+    data.pendingResult = data.pendingResult.substr(newData.length);
+  }
+  data.pendingBytes -= e.loaded - data.receivedBytes;
+  data.receivedResult = response;
+  data.receivedBytes = e.loaded;
+}
+
+function sendData(s) {
+  var xhr = new XMLHttpRequest();
+  xhr.open("POST", "progressserver.sjs?send");
+  xhr.sendAsBinary(s);
+}
+
+function closeConn() {
+  log("in closeConn");
+  var xhr = new XMLHttpRequest();
+  xhr.open("POST", "progressserver.sjs?close");
+  xhr.send();
+}
+
+var longString = "long";
+while(longString.length < 65536)
+  longString += longString;
+
+function utf8encode(s) {
+  return unescape(encodeURIComponent(s));
+}
+
+function bufferToString(buffer) {
+  return String.fromCharCode.apply(String, new Uint8Array(buffer));
+}
+
+function runTests() {
+  var xhr = new XMLHttpRequest();
+  xhr.onprogress = xhr.onload = xhr.onerror = xhr.onreadystatechange = xhr.onloadend = getEvent;
+
+  var responseTypes = [{ type: "text", text: true },
+                       { type: "arraybuffer", text: false, nodata: true },
+                       { type: "blob", text: false, nodata: true },
+                       { type: "document", text: true, nodata: true },
+                       { type: "moz-json", text: true, nodata: true },
+                       { type: "", text: true },
+                       { type: "moz-chunked-text", text: true, chunked: true },
+                       { type: "moz-chunked-arraybuffer", text: false, chunked: true },
+                      ];
+  var responseType;
+  while (responseType = responseTypes.shift()) {
+    let tests = [{ open: "Content-Type=text/plain", name: "simple test" },
+                 { data: "hello world" },
+                 { data: "\u0000\u0001\u0002\u0003" },
+                 { data: longString },
+                 { data: "x" },
+                 { close: true },
+                 { open: "Content-Type=text/plain&Content-Length=20", name: "with length", total: 20 },
+                 // 5 bytes from the "ready" in the open step
+                 { data: "abcde" },
+                 { data: "0123456789" },
+                 { close: true },
+                 { open: "Content-Type=application/xml", data: "ready", name: "without length, as xml" },
+                 { data: "<out>" },
+                 { data: "text" },
+                 { data: "</foo>invalid" },
+                 { close: true },
+                 { open: "Content-Type=text/plain;charset%3dutf-8", name: "utf8 data", encoded: true },
+                 { data: utf8encode("räksmörgås"), utf16: "räksmörgås" },
+                 { data: utf8encode("Å").substr(0,1), utf16: "" },
+                 { data: utf8encode("Å").substr(1), utf16: "Å" },
+                 { data: utf8encode("aöb").substr(0,2), utf16: "a" },
+                 { data: utf8encode("aöb").substr(2), utf16: "öb" },
+                 { data: utf8encode("a\u867Eb").substr(0,3), utf16: "a" },
+                 { data: utf8encode("a\u867Eb").substr(3,1), utf16: "\u867E" },
+                 { data: utf8encode("a\u867Eb").substr(4), utf16: "b" },
+                 { close: true },
+                 ];
+    let testState = { index: 0 };
+  
+    for (let i = 0; i < tests.length; ++i) {
+      let test = tests[i];
+      testState.index++;
+      if ("open" in test) {
+        log("opening " + testState.name);
+        testState = { name: test.name + " for " + responseType.type,
+                      index: 0,
+                      pendingResult: "ready",
+                      pendingBytes: 5,
+                      receivedResult: "",
+                      receivedBytes: 0,
+                      total: test.total,
+                      encoded: test.encoded,
+                      nodata: responseType.nodata,
+                      chunked: responseType.chunked,
+                      text: responseType.text };
+  
+        xhr.onreadystatechange = null;
+        xhr.open("POST", "progressserver.sjs?open&" + test.open);
+        xhr.responseType = responseType.type;
+        xhr.send("ready");
+        xhr.onreadystatechange = getEvent;
+
+        let e = yield;
+        is(e.type, "readystatechange", "should readystate to headers-received starting " + testState.name);
+        is(xhr.readyState, xhr.HEADERS_RECEIVED, "should be in state HEADERS_RECEIVED starting " + testState.name);
+  
+        e = yield;
+        is(e.type, "readystatechange", "should readystate to loading starting " + testState.name);
+        is(xhr.readyState, xhr.LOADING, "should be in state LOADING starting " + testState.name);
+        if (typeof testState.total == "undefined")
+          delete testState.total;
+      }
+      else if ("close" in test) {
+        log("closing");
+        closeConn();
+
+        e = yield;
+        is(e.type, "readystatechange", "should readystate to done closing " + testState.name);
+        is(xhr.readyState, xhr.DONE, "should be in state DONE closing " + testState.name);
+        log("readystate to 4");
+
+        if (responseType.chunked) {
+          xhr.responseType;
+          is(xhr.response, null, "chunked data has null response for " + testState.name);
+        }
+      
+        e = yield;
+        is(e.type, "load", "should fire load closing " + testState.name);
+        is(e.lengthComputable, true, "length should be computable during load closing " + testState.name);
+        log("got load");
+
+        if (responseType.chunked) {
+          is(xhr.response, null, "chunked data has null response for " + testState.name);
+        }
+      
+        e = yield;
+        is(e.type, "loadend", "should fire loadend closing " + testState.name);
+        is(e.lengthComputable, true, "length should be computable during loadend closing " + testState.name);
+        log("got loadend");
+
+        if (responseType.chunked) {
+          is(xhr.response, null, "chunked data has null response for " + testState.name);
+        }
+
+        if (!testState.nodata || responseType.chunked) {
+          // This branch intentionally left blank
+          // Under these conditions we check the response during updateProgress
+        }
+        else if (responseType.type === "arraybuffer") {
+          is(bufferToString(xhr.response), testState.pendingResult,
+             "full response for " + testState.name);
+        }
+        else if (responseType.type === "blob") {
+          let reader = new FileReader;
+          reader.readAsBinaryString(xhr.response);
+          reader.onloadend = getEvent;
+          yield;
+
+          is(reader.result, testState.pendingResult,
+             "full response in blob for " + testState.name);
+        }
+
+        testState.name = "";
+      }
+      else {
+        log("sending");
+        if (responseType.text) {
+          testState.pendingResult += "utf16" in test ? test.utf16 : test.data;
+        }
+        else {
+          testState.pendingResult += test.data;
+        }
+        testState.pendingBytes = test.data.length;
+        sendData(test.data);
+      }
+  
+      while(testState.pendingBytes) {
+        log("waiting for more bytes: " + testState.pendingBytes);
+        e = yield;
+        // Readystate can fire several times between each progress event.
+        if (e.type === "readystatechange")
+          continue;
+  
+        updateProgress(e, testState, "data for " + testState.name + "[" + testState.index + "]");
+        if (responseType.chunked) {
+          testState.receivedResult = "";
+        }
+      }
+
+      if (!testState.nodata) {
+        is(testState.pendingResult, "",
+           "should have consumed the expected result");
+      }
+
+      log("done with this test");
+    }
+  
+    is(testState.name, "", "forgot to close last test");
+  }
+
+  SimpleTest.finish();
+  yield;
+}
+
+</script>
+
+</body>
+</html>