[Pkg-javascript-commits] [libjs-fetch] 01/03: New upstream version 2.0.3

Ghislain Vaillant ghisvail-guest at moszumanska.debian.org
Fri Aug 4 14:57:57 UTC 2017


This is an automated email from the git hooks/post-receive script.

ghisvail-guest pushed a commit to branch master
in repository libjs-fetch.

commit 19e16b9b6ec420e9f3aa1e5cd164e5543352d6a5
Author: Ghislain Antony Vaillant <ghisvail at gmail.com>
Date:   Thu Aug 3 12:30:50 2017 +0100

    New upstream version 2.0.3
---
 .gitignore                    |    5 +
 .jshintrc                     |   25 +
 .travis.yml                   |   13 +
 LICENSE                       |   20 +
 MAINTAINING.md                |   26 +
 Makefile                      |   29 +
 README.md                     |  282 ++++++++++
 bower.json                    |   13 +
 examples/index.html           |   22 +
 fetch.js                      |  461 ++++++++++++++++
 package.json                  |   23 +
 script/phantomjs              |   36 ++
 script/saucelabs              |   66 +++
 script/saucelabs-api          |   31 ++
 script/server                 |  148 +++++
 script/test                   |    9 +
 test/.gitignore               |    1 +
 test/.jshintrc                |   19 +
 test/mocha-phantomjs-hooks.js |    9 +
 test/test-worker.html         |   45 ++
 test/test.html                |   63 +++
 test/test.js                  | 1189 +++++++++++++++++++++++++++++++++++++++++
 test/worker.js                |   38 ++
 23 files changed, 2573 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..17f90c3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.env
+bower_components/
+node_modules/
+sauce_connect/
+sauce_connect.log
diff --git a/.jshintrc b/.jshintrc
new file mode 100644
index 0000000..c451b54
--- /dev/null
+++ b/.jshintrc
@@ -0,0 +1,25 @@
+{
+  "curly": true,
+  "eqeqeq": true,
+  "es3": true,
+  "immed": true,
+  "indent": 2,
+  "latedef": true,
+  "newcap": true,
+  "noarg": true,
+  "quotmark": true,
+  "undef": true,
+  "unused": true,
+  "strict": true,
+  "trailing": true,
+  "asi": true,
+  "boss": true,
+  "esnext": true,
+  "eqnull": true,
+  "browser": true,
+  "worker": true,
+  "globals": {
+    "JSON": false,
+    "URLSearchParams": false
+  }
+}
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..583aa26
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,13 @@
+sudo: false
+language: node_js
+cache:
+  directories:
+  - phantomjs
+deploy:
+  provider: npm
+  email: mislav.marohnic at gmail.com
+  api_key:
+    secure: gt9g5/bXhxSKjxfFSPCdpWGJKBrSG8zdGRYgPouUgRqNeD2Ff4Nc8HGQTxp0OLKnP/jJ5FIru5jUur6LWzJCyEd+aNUEvFf5J078m3pzHN9AP2fiWUkKXcc5lKV0PQnI+JDRxJwd/PggtjubrneGfCzyFoys9apRrd/TzTGEtGw=
+  on:
+    tags: true
+    repo: github/fetch
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..0e319d5
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2014-2016 GitHub, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/MAINTAINING.md b/MAINTAINING.md
new file mode 100644
index 0000000..22185b7
--- /dev/null
+++ b/MAINTAINING.md
@@ -0,0 +1,26 @@
+# Maintaining
+
+## Releasing a new version
+
+This project follows [semver](http://semver.org/). So if you are making a bug
+fix, only increment the patch level "1.0.x". If any new files are added, a
+minor version "1.x.x" bump is in order.
+
+### Make a release commit
+
+To prepare the release commit:
+
+1. Update the npm [package.json](https://github.com/github/fetch/blob/master/package.json)
+`version` value.
+2. Make a single commit with the description as "Fetch 2.x.x".
+3. Finally, tag the commit with `v2.x.x`.
+
+```
+$ git pull
+$ vim package.json
+$ git add package.json
+$ git commit -m "Fetch 1.x.x"
+$ git tag v1.x.x
+$ git push
+$ git push --tags
+```
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..2c8fe43
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,29 @@
+test: node_modules/ lint
+	./script/test
+
+lint: node_modules/
+	./node_modules/.bin/jshint *.js test/*.js
+
+node_modules/:
+	npm install
+
+clean:
+	rm -rf ./bower_components ./node_modules
+
+ifeq ($(shell uname -s),Darwin)
+sauce_connect/bin/sc:
+	wget https://saucelabs.com/downloads/sc-4.3.16-osx.zip
+	unzip sc-4.3.16-osx.zip
+	mv sc-4.3.16-osx sauce_connect
+	rm sc-4.3.16-osx.zip
+else
+sauce_connect/bin/sc:
+	mkdir -p sauce_connect
+	curl -fsSL http://saucelabs.com/downloads/sc-4.3.16-linux.tar.gz | tar xz -C sauce_connect --strip-components 1
+endif
+
+phantomjs/bin/phantomjs:
+	mkdir -p phantomjs
+	wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 -O- | tar xj -C phantomjs --strip-components 1
+
+.PHONY: clean lint test
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..328cb49
--- /dev/null
+++ b/README.md
@@ -0,0 +1,282 @@
+# window.fetch polyfill
+
+The `fetch()` function is a Promise-based mechanism for programmatically making
+web requests in the browser. This project is a polyfill that implements a subset
+of the standard [Fetch specification][], enough to make `fetch` a viable
+replacement for most uses of XMLHttpRequest in traditional web applications.
+
+This project adheres to the [Open Code of Conduct][]. By participating, you are
+expected to uphold this code.
+
+## Table of Contents
+
+* [Read this first](#read-this-first)
+* [Installation](#installation)
+* [Usage](#usage)
+  * [HTML](#html)
+  * [JSON](#json)
+  * [Response metadata](#response-metadata)
+  * [Post form](#post-form)
+  * [Post JSON](#post-json)
+  * [File upload](#file-upload)
+  * [Caveats](#caveats)
+    * [Handling HTTP error statuses](#handling-http-error-statuses)
+    * [Sending cookies](#sending-cookies)
+    * [Receiving cookies](#receiving-cookies)
+    * [Obtaining the Response URL](#obtaining-the-response-url)
+* [Browser Support](#browser-support)
+
+## Read this first
+
+* If you believe you found a bug with how `fetch` behaves in Chrome or Firefox,
+  please **avoid opening an issue in this repository**. This project is a
+  _polyfill_, and since Chrome and Firefox both implement the `window.fetch`
+  function natively, no code from this project actually takes any effect in
+  these browsers. See [Browser support](#browser-support) for detailed
+  information.
+
+* If you have trouble **making a request to another domain** (a different
+  subdomain or port number also constitutes as another domain), please
+  familiarize yourself with all the intricacies and limitations of [CORS][]
+  requests. Because CORS requires participation of the server by implementing
+  specific HTTP response headers, it is often nontrivial to set up or debug.
+  CORS is exclusively handled by the browser's internal mechanisms which this
+  polyfill cannot influence.
+
+* If you have trouble **maintaining the user's session** or [CSRF][] protection
+  through `fetch` requests, please ensure that you've read and understood the
+  [Sending cookies](#sending-cookies) section.
+
+* If this polyfill **doesn't work under Node.js environments**, that is expected,
+  because this project is meant for web browsers only. You should ensure that your
+  application doesn't try to package and run this on the server.
+
+* If you have an idea for a new feature of `fetch`, please understand that we
+  are only ever going to add features and APIs that are a part of the
+  [Fetch specification][]. You should **submit your feature requests** to the
+  [repository of the specification](https://github.com/whatwg/fetch/issues)
+  itself, rather than this repository.
+
+## Installation
+
+* `npm install whatwg-fetch --save`; or
+
+* `bower install fetch`.
+
+You will also need a Promise polyfill for [older browsers](http://caniuse.com/#feat=promises).
+We recommend [taylorhakes/promise-polyfill](https://github.com/taylorhakes/promise-polyfill)
+for its small size and Promises/A+ compatibility.
+
+For use with webpack, add this package in the `entry` configuration option
+before your application entry point:
+
+```javascript
+entry: ['whatwg-fetch', ...]
+```
+
+For Babel and ES2015+, make sure to import the file:
+
+```javascript
+import 'whatwg-fetch'
+```
+
+## Usage
+
+For a more comprehensive API reference that this polyfill supports, refer to
+https://github.github.io/fetch/.
+
+### HTML
+
+```javascript
+fetch('/users.html')
+  .then(function(response) {
+    return response.text()
+  }).then(function(body) {
+    document.body.innerHTML = body
+  })
+```
+
+### JSON
+
+```javascript
+fetch('/users.json')
+  .then(function(response) {
+    return response.json()
+  }).then(function(json) {
+    console.log('parsed json', json)
+  }).catch(function(ex) {
+    console.log('parsing failed', ex)
+  })
+```
+
+### Response metadata
+
+```javascript
+fetch('/users.json').then(function(response) {
+  console.log(response.headers.get('Content-Type'))
+  console.log(response.headers.get('Date'))
+  console.log(response.status)
+  console.log(response.statusText)
+})
+```
+
+### Post form
+
+```javascript
+var form = document.querySelector('form')
+
+fetch('/users', {
+  method: 'POST',
+  body: new FormData(form)
+})
+```
+
+### Post JSON
+
+```javascript
+fetch('/users', {
+  method: 'POST',
+  headers: {
+    'Content-Type': 'application/json'
+  },
+  body: JSON.stringify({
+    name: 'Hubot',
+    login: 'hubot',
+  })
+})
+```
+
+### File upload
+
+```javascript
+var input = document.querySelector('input[type="file"]')
+
+var data = new FormData()
+data.append('file', input.files[0])
+data.append('user', 'hubot')
+
+fetch('/avatars', {
+  method: 'POST',
+  body: data
+})
+```
+
+### Caveats
+
+The `fetch` specification differs from `jQuery.ajax()` in mainly two ways that
+bear keeping in mind:
+
+* The Promise returned from `fetch()` **won't reject on HTTP error status**
+  even if the response is an HTTP 404 or 500. Instead, it will resolve normally,
+  and it will only reject on network failure or if anything prevented the
+  request from completing.
+
+* By default, `fetch` **won't send or receive any cookies** from the server,
+  resulting in unauthenticated requests if the site relies on maintaining a user
+  session. See [Sending cookies](#sending-cookies) for how to opt into cookie
+  handling.
+
+#### Handling HTTP error statuses
+
+To have `fetch` Promise reject on HTTP error statuses, i.e. on any non-2xx
+status, define a custom response handler:
+
+```javascript
+function checkStatus(response) {
+  if (response.status >= 200 && response.status < 300) {
+    return response
+  } else {
+    var error = new Error(response.statusText)
+    error.response = response
+    throw error
+  }
+}
+
+function parseJSON(response) {
+  return response.json()
+}
+
+fetch('/users')
+  .then(checkStatus)
+  .then(parseJSON)
+  .then(function(data) {
+    console.log('request succeeded with JSON response', data)
+  }).catch(function(error) {
+    console.log('request failed', error)
+  })
+```
+
+#### Sending cookies
+
+To automatically send cookies for the current domain, the `credentials` option
+must be provided:
+
+```javascript
+fetch('/users', {
+  credentials: 'same-origin'
+})
+```
+
+The "same-origin" value makes `fetch` behave similarly to XMLHttpRequest with
+regards to cookies. Otherwise, cookies won't get sent, resulting in these
+requests not preserving the authentication session.
+
+For [CORS][] requests, use the "include" value to allow sending credentials to
+other domains:
+
+```javascript
+fetch('https://example.com:1234/users', {
+  credentials: 'include'
+})
+```
+
+#### Receiving cookies
+
+As with XMLHttpRequest, the `Set-Cookie` response header returned from the
+server is a [forbidden header name][] and therefore can't be programmatically
+read with `response.headers.get()`. Instead, it's the browser's responsibility
+to handle new cookies being set (if applicable to the current URL). Unless they
+are HTTP-only, new cookies will be available through `document.cookie`.
+
+Bear in mind that the default behavior of `fetch` is to ignore the `Set-Cookie`
+header completely. To opt into accepting cookies from the server, you must use
+the `credentials` option.
+
+#### Obtaining the Response URL
+
+Due to limitations of XMLHttpRequest, the `response.url` value might not be
+reliable after HTTP redirects on older browsers.
+
+The solution is to configure the server to set the response HTTP header
+`X-Request-URL` to the current URL after any redirect that might have happened.
+It should be safe to set it unconditionally.
+
+``` ruby
+# Ruby on Rails controller example
+response.headers['X-Request-URL'] = request.url
+```
+
+This server workaround is necessary if you need reliable `response.url` in
+Firefox < 32, Chrome < 37, Safari, or IE.
+
+## Browser Support
+
+- Chrome
+- Firefox
+- Safari 6.1+
+- Internet Explorer 10+
+
+Note: modern browsers such as Chrome, Firefox, and Microsoft Edge contain native
+implementations of `window.fetch`, therefore the code from this polyfill doesn't
+have any effect on those browsers. If you believe you've encountered an error
+with how `window.fetch` is implemented in any of these browsers, you should file
+an issue with that browser vendor instead of this project.
+
+
+  [fetch specification]: https://fetch.spec.whatwg.org
+  [open code of conduct]: http://todogroup.org/opencodeofconduct/#fetch/opensource@github.com
+  [cors]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
+    "Cross-origin resource sharing"
+  [csrf]: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet
+    "Cross-site request forgery"
+  [forbidden header name]: https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name
diff --git a/bower.json b/bower.json
new file mode 100644
index 0000000..266f12d
--- /dev/null
+++ b/bower.json
@@ -0,0 +1,13 @@
+{
+  "name": "fetch",
+  "main": "fetch.js",
+  "ignore": [
+    ".*",
+    "*.md",
+    "examples/",
+    "Makefile",
+    "package.json",
+    "script/",
+    "test/"
+  ]
+}
diff --git a/examples/index.html b/examples/index.html
new file mode 100644
index 0000000..546a695
--- /dev/null
+++ b/examples/index.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <script src="../fetch.js"></script>
+</head>
+<body>
+  <script>
+    var result = fetch('https://api.github.com')
+
+    result.then(function(response) {
+      console.log('response', response)
+      console.log('header', response.headers.get('Content-Type'))
+      return response.text()
+    }).then(function(text) {
+      console.log('got text', text)
+    }).catch(function(ex) {
+      console.log('failed', ex)
+    })
+  </script>
+</body>
+</html>
diff --git a/fetch.js b/fetch.js
new file mode 100644
index 0000000..6bac6b3
--- /dev/null
+++ b/fetch.js
@@ -0,0 +1,461 @@
+(function(self) {
+  'use strict';
+
+  if (self.fetch) {
+    return
+  }
+
+  var support = {
+    searchParams: 'URLSearchParams' in self,
+    iterable: 'Symbol' in self && 'iterator' in Symbol,
+    blob: 'FileReader' in self && 'Blob' in self && (function() {
+      try {
+        new Blob()
+        return true
+      } catch(e) {
+        return false
+      }
+    })(),
+    formData: 'FormData' in self,
+    arrayBuffer: 'ArrayBuffer' in self
+  }
+
+  if (support.arrayBuffer) {
+    var viewClasses = [
+      '[object Int8Array]',
+      '[object Uint8Array]',
+      '[object Uint8ClampedArray]',
+      '[object Int16Array]',
+      '[object Uint16Array]',
+      '[object Int32Array]',
+      '[object Uint32Array]',
+      '[object Float32Array]',
+      '[object Float64Array]'
+    ]
+
+    var isDataView = function(obj) {
+      return obj && DataView.prototype.isPrototypeOf(obj)
+    }
+
+    var isArrayBufferView = ArrayBuffer.isView || function(obj) {
+      return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1
+    }
+  }
+
+  function normalizeName(name) {
+    if (typeof name !== 'string') {
+      name = String(name)
+    }
+    if (/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(name)) {
+      throw new TypeError('Invalid character in header field name')
+    }
+    return name.toLowerCase()
+  }
+
+  function normalizeValue(value) {
+    if (typeof value !== 'string') {
+      value = String(value)
+    }
+    return value
+  }
+
+  // Build a destructive iterator for the value list
+  function iteratorFor(items) {
+    var iterator = {
+      next: function() {
+        var value = items.shift()
+        return {done: value === undefined, value: value}
+      }
+    }
+
+    if (support.iterable) {
+      iterator[Symbol.iterator] = function() {
+        return iterator
+      }
+    }
+
+    return iterator
+  }
+
+  function Headers(headers) {
+    this.map = {}
+
+    if (headers instanceof Headers) {
+      headers.forEach(function(value, name) {
+        this.append(name, value)
+      }, this)
+    } else if (Array.isArray(headers)) {
+      headers.forEach(function(header) {
+        this.append(header[0], header[1])
+      }, this)
+    } else if (headers) {
+      Object.getOwnPropertyNames(headers).forEach(function(name) {
+        this.append(name, headers[name])
+      }, this)
+    }
+  }
+
+  Headers.prototype.append = function(name, value) {
+    name = normalizeName(name)
+    value = normalizeValue(value)
+    var oldValue = this.map[name]
+    this.map[name] = oldValue ? oldValue+','+value : value
+  }
+
+  Headers.prototype['delete'] = function(name) {
+    delete this.map[normalizeName(name)]
+  }
+
+  Headers.prototype.get = function(name) {
+    name = normalizeName(name)
+    return this.has(name) ? this.map[name] : null
+  }
+
+  Headers.prototype.has = function(name) {
+    return this.map.hasOwnProperty(normalizeName(name))
+  }
+
+  Headers.prototype.set = function(name, value) {
+    this.map[normalizeName(name)] = normalizeValue(value)
+  }
+
+  Headers.prototype.forEach = function(callback, thisArg) {
+    for (var name in this.map) {
+      if (this.map.hasOwnProperty(name)) {
+        callback.call(thisArg, this.map[name], name, this)
+      }
+    }
+  }
+
+  Headers.prototype.keys = function() {
+    var items = []
+    this.forEach(function(value, name) { items.push(name) })
+    return iteratorFor(items)
+  }
+
+  Headers.prototype.values = function() {
+    var items = []
+    this.forEach(function(value) { items.push(value) })
+    return iteratorFor(items)
+  }
+
+  Headers.prototype.entries = function() {
+    var items = []
+    this.forEach(function(value, name) { items.push([name, value]) })
+    return iteratorFor(items)
+  }
+
+  if (support.iterable) {
+    Headers.prototype[Symbol.iterator] = Headers.prototype.entries
+  }
+
+  function consumed(body) {
+    if (body.bodyUsed) {
+      return Promise.reject(new TypeError('Already read'))
+    }
+    body.bodyUsed = true
+  }
+
+  function fileReaderReady(reader) {
+    return new Promise(function(resolve, reject) {
+      reader.onload = function() {
+        resolve(reader.result)
+      }
+      reader.onerror = function() {
+        reject(reader.error)
+      }
+    })
+  }
+
+  function readBlobAsArrayBuffer(blob) {
+    var reader = new FileReader()
+    var promise = fileReaderReady(reader)
+    reader.readAsArrayBuffer(blob)
+    return promise
+  }
+
+  function readBlobAsText(blob) {
+    var reader = new FileReader()
+    var promise = fileReaderReady(reader)
+    reader.readAsText(blob)
+    return promise
+  }
+
+  function readArrayBufferAsText(buf) {
+    var view = new Uint8Array(buf)
+    var chars = new Array(view.length)
+
+    for (var i = 0; i < view.length; i++) {
+      chars[i] = String.fromCharCode(view[i])
+    }
+    return chars.join('')
+  }
+
+  function bufferClone(buf) {
+    if (buf.slice) {
+      return buf.slice(0)
+    } else {
+      var view = new Uint8Array(buf.byteLength)
+      view.set(new Uint8Array(buf))
+      return view.buffer
+    }
+  }
+
+  function Body() {
+    this.bodyUsed = false
+
+    this._initBody = function(body) {
+      this._bodyInit = body
+      if (!body) {
+        this._bodyText = ''
+      } else if (typeof body === 'string') {
+        this._bodyText = body
+      } else if (support.blob && Blob.prototype.isPrototypeOf(body)) {
+        this._bodyBlob = body
+      } else if (support.formData && FormData.prototype.isPrototypeOf(body)) {
+        this._bodyFormData = body
+      } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
+        this._bodyText = body.toString()
+      } else if (support.arrayBuffer && support.blob && isDataView(body)) {
+        this._bodyArrayBuffer = bufferClone(body.buffer)
+        // IE 10-11 can't handle a DataView body.
+        this._bodyInit = new Blob([this._bodyArrayBuffer])
+      } else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {
+        this._bodyArrayBuffer = bufferClone(body)
+      } else {
+        throw new Error('unsupported BodyInit type')
+      }
+
+      if (!this.headers.get('content-type')) {
+        if (typeof body === 'string') {
+          this.headers.set('content-type', 'text/plain;charset=UTF-8')
+        } else if (this._bodyBlob && this._bodyBlob.type) {
+          this.headers.set('content-type', this._bodyBlob.type)
+        } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {
+          this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8')
+        }
+      }
+    }
+
+    if (support.blob) {
+      this.blob = function() {
+        var rejected = consumed(this)
+        if (rejected) {
+          return rejected
+        }
+
+        if (this._bodyBlob) {
+          return Promise.resolve(this._bodyBlob)
+        } else if (this._bodyArrayBuffer) {
+          return Promise.resolve(new Blob([this._bodyArrayBuffer]))
+        } else if (this._bodyFormData) {
+          throw new Error('could not read FormData body as blob')
+        } else {
+          return Promise.resolve(new Blob([this._bodyText]))
+        }
+      }
+
+      this.arrayBuffer = function() {
+        if (this._bodyArrayBuffer) {
+          return consumed(this) || Promise.resolve(this._bodyArrayBuffer)
+        } else {
+          return this.blob().then(readBlobAsArrayBuffer)
+        }
+      }
+    }
+
+    this.text = function() {
+      var rejected = consumed(this)
+      if (rejected) {
+        return rejected
+      }
+
+      if (this._bodyBlob) {
+        return readBlobAsText(this._bodyBlob)
+      } else if (this._bodyArrayBuffer) {
+        return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer))
+      } else if (this._bodyFormData) {
+        throw new Error('could not read FormData body as text')
+      } else {
+        return Promise.resolve(this._bodyText)
+      }
+    }
+
+    if (support.formData) {
+      this.formData = function() {
+        return this.text().then(decode)
+      }
+    }
+
+    this.json = function() {
+      return this.text().then(JSON.parse)
+    }
+
+    return this
+  }
+
+  // HTTP methods whose capitalization should be normalized
+  var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']
+
+  function normalizeMethod(method) {
+    var upcased = method.toUpperCase()
+    return (methods.indexOf(upcased) > -1) ? upcased : method
+  }
+
+  function Request(input, options) {
+    options = options || {}
+    var body = options.body
+
+    if (input instanceof Request) {
+      if (input.bodyUsed) {
+        throw new TypeError('Already read')
+      }
+      this.url = input.url
+      this.credentials = input.credentials
+      if (!options.headers) {
+        this.headers = new Headers(input.headers)
+      }
+      this.method = input.method
+      this.mode = input.mode
+      if (!body && input._bodyInit != null) {
+        body = input._bodyInit
+        input.bodyUsed = true
+      }
+    } else {
+      this.url = String(input)
+    }
+
+    this.credentials = options.credentials || this.credentials || 'omit'
+    if (options.headers || !this.headers) {
+      this.headers = new Headers(options.headers)
+    }
+    this.method = normalizeMethod(options.method || this.method || 'GET')
+    this.mode = options.mode || this.mode || null
+    this.referrer = null
+
+    if ((this.method === 'GET' || this.method === 'HEAD') && body) {
+      throw new TypeError('Body not allowed for GET or HEAD requests')
+    }
+    this._initBody(body)
+  }
+
+  Request.prototype.clone = function() {
+    return new Request(this, { body: this._bodyInit })
+  }
+
+  function decode(body) {
+    var form = new FormData()
+    body.trim().split('&').forEach(function(bytes) {
+      if (bytes) {
+        var split = bytes.split('=')
+        var name = split.shift().replace(/\+/g, ' ')
+        var value = split.join('=').replace(/\+/g, ' ')
+        form.append(decodeURIComponent(name), decodeURIComponent(value))
+      }
+    })
+    return form
+  }
+
+  function parseHeaders(rawHeaders) {
+    var headers = new Headers()
+    rawHeaders.split(/\r?\n/).forEach(function(line) {
+      var parts = line.split(':')
+      var key = parts.shift().trim()
+      if (key) {
+        var value = parts.join(':').trim()
+        headers.append(key, value)
+      }
+    })
+    return headers
+  }
+
+  Body.call(Request.prototype)
+
+  function Response(bodyInit, options) {
+    if (!options) {
+      options = {}
+    }
+
+    this.type = 'default'
+    this.status = 'status' in options ? options.status : 200
+    this.ok = this.status >= 200 && this.status < 300
+    this.statusText = 'statusText' in options ? options.statusText : 'OK'
+    this.headers = new Headers(options.headers)
+    this.url = options.url || ''
+    this._initBody(bodyInit)
+  }
+
+  Body.call(Response.prototype)
+
+  Response.prototype.clone = function() {
+    return new Response(this._bodyInit, {
+      status: this.status,
+      statusText: this.statusText,
+      headers: new Headers(this.headers),
+      url: this.url
+    })
+  }
+
+  Response.error = function() {
+    var response = new Response(null, {status: 0, statusText: ''})
+    response.type = 'error'
+    return response
+  }
+
+  var redirectStatuses = [301, 302, 303, 307, 308]
+
+  Response.redirect = function(url, status) {
+    if (redirectStatuses.indexOf(status) === -1) {
+      throw new RangeError('Invalid status code')
+    }
+
+    return new Response(null, {status: status, headers: {location: url}})
+  }
+
+  self.Headers = Headers
+  self.Request = Request
+  self.Response = Response
+
+  self.fetch = function(input, init) {
+    return new Promise(function(resolve, reject) {
+      var request = new Request(input, init)
+      var xhr = new XMLHttpRequest()
+
+      xhr.onload = function() {
+        var options = {
+          status: xhr.status,
+          statusText: xhr.statusText,
+          headers: parseHeaders(xhr.getAllResponseHeaders() || '')
+        }
+        options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL')
+        var body = 'response' in xhr ? xhr.response : xhr.responseText
+        resolve(new Response(body, options))
+      }
+
+      xhr.onerror = function() {
+        reject(new TypeError('Network request failed'))
+      }
+
+      xhr.ontimeout = function() {
+        reject(new TypeError('Network request failed'))
+      }
+
+      xhr.open(request.method, request.url, true)
+
+      if (request.credentials === 'include') {
+        xhr.withCredentials = true
+      }
+
+      if ('responseType' in xhr && support.blob) {
+        xhr.responseType = 'blob'
+      }
+
+      request.headers.forEach(function(value, name) {
+        xhr.setRequestHeader(name, value)
+      })
+
+      xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit)
+    })
+  }
+  self.fetch.polyfill = true
+})(typeof self !== 'undefined' ? self : this);
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..e6d80cc
--- /dev/null
+++ b/package.json
@@ -0,0 +1,23 @@
+{
+  "name": "whatwg-fetch",
+  "description": "A window.fetch polyfill.",
+  "version": "2.0.3",
+  "main": "fetch.js",
+  "repository": "github/fetch",
+  "license": "MIT",
+  "devDependencies": {
+    "chai": "1.10.0",
+    "jshint": "2.8.0",
+    "mocha": "2.1.0",
+    "mocha-phantomjs-core": "2.0.1",
+    "promise-polyfill": "6.0.2",
+    "url-search-params": "0.6.1"
+  },
+  "files": [
+    "LICENSE",
+    "fetch.js"
+  ],
+  "scripts": {
+    "test": "make"
+  }
+}
diff --git a/script/phantomjs b/script/phantomjs
new file mode 100755
index 0000000..935a7fa
--- /dev/null
+++ b/script/phantomjs
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+set -e
+
+port=3900
+
+# Find next available port
+while lsof -i :$((++port)) >/dev/null; do true; done
+
+# Spin a test server in the background
+node ./script/server $port &>/dev/null &
+server_pid=$!
+trap "kill $server_pid" INT EXIT
+
+STATUS=0
+
+reporter=dot
+[ -z "$CI" ] || reporter=spec
+
+if [ -n "$TRAVIS" ]; then
+  make phantomjs/bin/phantomjs
+  export PATH="$PWD/phantomjs/bin:$PATH"
+fi
+
+run() {
+  phantomjs ./node_modules/mocha-phantomjs-core/mocha-phantomjs-core.js \
+    "$1" $reporter "{\"useColors\":true, \"hooks\":\"$PWD/test/mocha-phantomjs-hooks.js\"}" \
+    || STATUS=$?
+}
+
+[ -z "$CI" ] || echo "phantomjs $(phantomjs -v)"
+
+run "http://localhost:$port/"
+run "http://localhost:$port/test/test-worker.html"
+
+exit $STATUS
diff --git a/script/saucelabs b/script/saucelabs
new file mode 100755
index 0000000..8c99620
--- /dev/null
+++ b/script/saucelabs
@@ -0,0 +1,66 @@
+#!/bin/bash
+
+set -e
+
+port=8080
+
+# Spin a test server in the background
+node ./script/server $port &>/dev/null &
+server_pid=$!
+trap "kill $server_pid" INT EXIT
+
+make sauce_connect/bin/sc
+sauce_ready="${TMPDIR:-/tmp}/sauce-ready.$$"
+sauce_connect/bin/sc -u "$SAUCE_USERNAME" -k "$SAUCE_ACCESS_KEY" \
+  -i "$TRAVIS_JOB_NUMBER" -l sauce_connect.log -f "$sauce_ready" &>/dev/null &
+sauce_pid=$!
+trap "kill $sauce_pid" INT EXIT
+
+sauce_waited=0
+while [ ! -f "$sauce_ready" ]; do
+  if [ "$sauce_waited" -gt 60000 ]; then
+    echo "sauce_connect failed to start within 60 seconds" >&2
+    exit 1
+  fi
+  sleep .01
+  sauce_waited=$((sauce_waited + 10))
+done
+echo "sauce_connect started within $sauce_waited ms"
+rm -f "$sauce_ready"
+
+job="$(./script/saucelabs-api --raw "js-tests" <<JSON
+  { "public": "public",
+    "build": "$TRAVIS_BUILD_NUMBER",
+    "tags": ["$TRAVIS_PULL_REQUEST", "$TRAVIS_BRANCH"],
+    "tunnel-identifier": "$TRAVIS_JOB_NUMBER",
+    "platforms": [["$SAUCE_PLATFORM", "$SAUCE_BROWSER", "$SAUCE_VERSION"]],
+    "url": "http://localhost:$port/",
+    "framework": "mocha"
+  }
+JSON
+)"
+
+while sleep 5; do
+  result=$(./script/saucelabs-api "js-tests/status" <<<"$job")
+  if grep -q '.status: test error' <<<"$result"; then
+    echo
+    echo "$result" >&2
+    exit 1
+  fi
+  grep -q "^completed: true" <<<"$result" && break
+  echo -n "."
+done
+
+echo
+
+awk '
+  /result\.tests:/ { tests+=$(NF) }
+  /result\.passes:/ { passes+=$(NF) }
+  /result\.pending:/ { pending+=$(NF) }
+  /result\.failures:/ { failures+=$(NF) }
+  /\.url:/ { print $(NF) }
+  END {
+    printf "%d passed, %d pending, %d failures\n", passes, pending, failures
+    if (failures > 0 || tests != passes + pending || tests == 0) exit 1
+  }
+' <<<"$result"
diff --git a/script/saucelabs-api b/script/saucelabs-api
new file mode 100755
index 0000000..344c320
--- /dev/null
+++ b/script/saucelabs-api
@@ -0,0 +1,31 @@
+#!/bin/bash
+set -e
+set -o pipefail
+
+raw=""
+if [ "$1" = "--raw" ]; then
+  raw="1"
+  shift 1
+fi
+
+endpoint="$1"
+
+curl -fsS -X POST "https://saucelabs.com/rest/v1/$SAUCE_USERNAME/${endpoint}" \
+  -u "$SAUCE_USERNAME:$SAUCE_ACCESS_KEY" \
+  -H "Content-Type: application/json" -d "@-" | \
+{
+  if [ -n "$raw" ]; then
+    cat
+  else
+    ruby -rjson -e '
+      dump = lambda do |obj, ns|
+        case obj
+        when Array then obj.each_with_index { |v, i| dump.call(v, [ns, i]) }
+        when Hash then obj.each { |k, v| dump.call(v, [ns, k]) }
+        else puts "%s: %s" % [ ns.flatten.compact.join("."), obj.to_s ]
+        end
+      end
+      dump.call JSON.parse(STDIN.read), nil
+    '
+  fi
+}
diff --git a/script/server b/script/server
new file mode 100755
index 0000000..00993f8
--- /dev/null
+++ b/script/server
@@ -0,0 +1,148 @@
+#!/usr/bin/env node
+
+var port = Number(process.argv[2] || 3000)
+
+var fs = require('fs')
+var http = require('http');
+var url = require('url');
+var querystring = require('querystring');
+
+var routes = {
+  '/request': function(res, req) {
+    res.writeHead(200, {'Content-Type': 'application/json'});
+    var data = ''
+    req.on('data', function(c) { data += c })
+    req.on('end', function() {
+      res.end(JSON.stringify({
+        method: req.method,
+        url: req.url,
+        headers: req.headers,
+        data: data
+      }));
+    })
+  },
+  '/hello': function(res, req) {
+    res.writeHead(200, {
+      'Content-Type': 'text/plain',
+      'X-Request-URL': 'http://' + req.headers.host + req.url
+    });
+    res.end('hi');
+  },
+  '/hello/utf8': function(res) {
+    res.writeHead(200, {
+      'Content-Type': 'text/plain; charset=utf-8'
+    });
+    // "hello"
+    var buf = new Buffer([104, 101, 108, 108, 111]);
+    res.end(buf);
+  },
+  '/hello/utf16le': function(res) {
+    res.writeHead(200, {
+      'Content-Type': 'text/plain; charset=utf-16le'
+    });
+    // "hello"
+    var buf = new Buffer([104, 0, 101, 0, 108, 0, 108, 0, 111, 0]);
+    res.end(buf);
+  },
+  '/binary': function(res) {
+    res.writeHead(200, {'Content-Type': 'application/octet-stream'});
+    var buf = new Buffer(256);
+    for (var i = 0; i < 256; i++) {
+      buf[i] = i;
+    }
+    res.end(buf);
+  },
+  '/redirect/301': function(res) {
+    res.writeHead(301, {'Location': '/hello'});
+    res.end();
+  },
+  '/redirect/302': function(res) {
+    res.writeHead(302, {'Location': '/hello'});
+    res.end();
+  },
+  '/redirect/303': function(res) {
+    res.writeHead(303, {'Location': '/hello'});
+    res.end();
+  },
+  '/redirect/307': function(res) {
+    res.writeHead(307, {'Location': '/hello'});
+    res.end();
+  },
+  '/redirect/308': function(res) {
+    res.writeHead(308, {'Location': '/hello'});
+    res.end();
+  },
+  '/boom': function(res) {
+    res.writeHead(500, {'Content-Type': 'text/plain'});
+    res.end('boom');
+  },
+  '/empty': function(res) {
+    res.writeHead(204);
+    res.end();
+  },
+  '/error': function(res) {
+    res.destroy();
+  },
+  '/form': function(res) {
+    res.writeHead(200, {'Content-Type': 'application/x-www-form-urlencoded'});
+    res.end('number=1&space=one+two&empty=&encoded=a%2Bb&');
+  },
+  '/json': function(res) {
+    res.writeHead(200, {'Content-Type': 'application/json'});
+    res.end(JSON.stringify({name: 'Hubot', login: 'hubot'}));
+  },
+  '/json-error': function(res) {
+    res.writeHead(200, {'Content-Type': 'application/json'});
+    res.end('not json {');
+  },
+  '/cookie': function(res, req) {
+    var setCookie, cookie
+    var params = querystring.parse(url.parse(req.url).query);
+    if (params.name && params.value) {
+      setCookie = [params.name, params.value].join('=');
+    }
+    if (params.name) {
+      cookie = querystring.parse(req.headers['cookie'], '; ')[params.name];
+    }
+    res.writeHead(200, {'Content-Type': 'text/plain', 'Set-Cookie': setCookie});
+    res.end(cookie);
+  },
+  '/headers': function(res) {
+    res.writeHead(200, {
+      'Date': 'Mon, 13 Oct 2014 21:02:27 GMT',
+      'Content-Type': 'text/html; charset=utf-8'
+    });
+    res.end();
+  }
+};
+
+var types = {
+  js: 'application/javascript',
+  css: 'text/css',
+  html: 'text/html',
+  txt: 'text/plain'
+};
+
+server = http.createServer(function(req, res) {
+  var pathname = url.parse(req.url).pathname;
+  var route = routes[pathname];
+  if (route) {
+    route(res, req);
+  } else {
+    if (pathname == '/') pathname = '/test/test.html'
+    fs.readFile(__dirname + '/..' + pathname, function(err, data) {
+      if (err) {
+        res.writeHead(404, {'Content-Type': types.txt});
+        res.end('Not Found');
+      } else {
+        var ext = (pathname.match(/\.([^\/]+)$/) || [])[1]
+        res.writeHead(200, {'Content-Type': types[ext] || types.txt});
+        res.end(data);
+      }
+    });
+  }
+});
+
+console.warn("Started test server on localhost:" + port);
+server.listen(port);
+
diff --git a/script/test b/script/test
new file mode 100755
index 0000000..bc471d4
--- /dev/null
+++ b/script/test
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+set -e
+
+if [ -n "$SAUCE_BROWSER" ]; then
+  ./script/saucelabs
+else
+  ./script/phantomjs
+fi
diff --git a/test/.gitignore b/test/.gitignore
new file mode 100644
index 0000000..e6011a5
--- /dev/null
+++ b/test/.gitignore
@@ -0,0 +1 @@
+server.pid
diff --git a/test/.jshintrc b/test/.jshintrc
new file mode 100644
index 0000000..300079e
--- /dev/null
+++ b/test/.jshintrc
@@ -0,0 +1,19 @@
+{
+  "extends": "../.jshintrc",
+  "es3": false,
+  "strict": false,
+  "sub": true,
+  "globals": {
+    "fetch": false,
+    "Headers": false,
+    "Request": false,
+    "Response": false,
+    "mocha": false,
+    "chai": false,
+    "suite": false,
+    "setup": false,
+    "suiteSetup": false,
+    "test": false,
+    "assert": false
+  }
+}
diff --git a/test/mocha-phantomjs-hooks.js b/test/mocha-phantomjs-hooks.js
new file mode 100644
index 0000000..27227f7
--- /dev/null
+++ b/test/mocha-phantomjs-hooks.js
@@ -0,0 +1,9 @@
+/* globals exports */
+exports.beforeStart = function(context) {
+  var originalResourceError = context.page.onResourceError
+  context.page.onResourceError = function(resErr) {
+    if (!/\/boom$/.test(resErr.url)) {
+      originalResourceError(resErr)
+    }
+  }
+}
diff --git a/test/test-worker.html b/test/test-worker.html
new file mode 100644
index 0000000..71c259f
--- /dev/null
+++ b/test/test-worker.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Fetch Worker Tests</title>
+  <link rel="stylesheet" href="/node_modules/mocha/mocha.css" />
+</head>
+<body>
+  <div id="mocha"></div>
+  <script src="/node_modules/url-search-params/build/url-search-params.js"></script>
+  <script src="/node_modules/mocha/mocha.js"></script>
+
+  <script>
+    if (self.initMochaPhantomJS) {
+      self.initMochaPhantomJS()
+    }
+
+    mocha.setup('tdd')
+    mocha.suite.suites.unshift(Mocha.Suite.create(mocha.suite, "worker"))
+
+    var worker = new Worker('/test/worker.js')
+
+    worker.addEventListener('message', function(e) {
+      switch (e.data.name) {
+        case 'pass':
+          test(e.data.title, function() {})
+          break
+        case 'pending':
+          test(e.data.title)
+          break
+        case 'fail':
+          test(e.data.title, function() {
+            var err = new Error(e.data.message)
+            err.stack = e.data.stack
+            throw err
+          })
+          break
+        case 'end':
+          mocha.run()
+          break
+      }
+    })
+  </script>
+</body>
+</html>
diff --git a/test/test.html b/test/test.html
new file mode 100644
index 0000000..8a1e48f
--- /dev/null
+++ b/test/test.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Fetch Tests</title>
+  <link rel="stylesheet" href="/node_modules/mocha/mocha.css" />
+</head>
+<body>
+  <div id="mocha"></div>
+  <script>
+    window.onerror = function(err) {
+      var container = document.getElementById('mocha')
+      var el = document.createElement('p')
+      el.textContent = err.toString()
+      el.style = 'color:#c00'
+      container.insertBefore(el, container.firstChild)
+    }
+  </script>
+  <script src="/node_modules/url-search-params/build/url-search-params.js"></script>
+  <script src="/node_modules/chai/chai.js"></script>
+  <script src="/node_modules/mocha/mocha.js"></script>
+  <script>
+    if (self.initMochaPhantomJS) {
+      self.initMochaPhantomJS()
+    }
+
+    if (self.mocha && mocha.setup) {
+      mocha.setup('tdd')
+      self.assert = chai.assert
+    } else {
+      document.write('<p>Error: please run <code>make</code> to install dependencies and try again.</p>')
+    }
+  </script>
+
+  <script src="/node_modules/promise-polyfill/promise.js"></script>
+  <script src="/test/test.js"></script>
+  <script src="/fetch.js"></script>
+
+  <script>
+    var runner = mocha.run();
+
+    var failedTests = [];
+
+    runner.on('end', function(){
+      window.mochaResults = runner.stats;
+      window.mochaResults.reports = failedTests;
+    });
+
+    runner.on('fail', function(test, err){
+      function flattenTitles(test){
+        var titles = [];
+        while (test.parent.title){
+          titles.push(test.parent.title);
+          test = test.parent;
+        }
+        return titles.reverse();
+      };
+
+      failedTests.push({name: test.title, result: false, message: err.message, stack: err.stack, titles: flattenTitles(test) });
+    });
+  </script>
+</body>
+</html>
diff --git a/test/test.js b/test/test.js
new file mode 100644
index 0000000..47fa98e
--- /dev/null
+++ b/test/test.js
@@ -0,0 +1,1189 @@
+var support = {
+  searchParams: 'URLSearchParams' in self,
+  url: (function(url) {
+    try {
+      return new URL(url).toString() === url
+    } catch(e) {
+      return false
+    }
+  })('http://example.com/'),
+  blob: 'FileReader' in self && 'Blob' in self && (function() {
+    try {
+      new Blob()
+      return true
+    } catch(e) {
+      return false
+    }
+  })(),
+  formData: 'FormData' in self,
+  arrayBuffer: 'ArrayBuffer' in self,
+  patch: !/PhantomJS/.test(navigator.userAgent),
+  permanentRedirect: !/PhantomJS|Trident/.test(navigator.userAgent)
+}
+
+function readBlobAsText(blob) {
+  if ('FileReader' in self) {
+    return new Promise(function(resolve, reject) {
+      var reader = new FileReader()
+      reader.onload = function() {
+        resolve(reader.result)
+      }
+      reader.onerror = function() {
+        reject(reader.error)
+      }
+      reader.readAsText(blob)
+    })
+  } else if ('FileReaderSync' in self) {
+    return new FileReaderSync().readAsText(blob)
+  } else {
+    throw new ReferenceError('FileReader is not defined')
+  }
+}
+
+function readBlobAsBytes(blob) {
+  if ('FileReader' in self) {
+    return new Promise(function(resolve, reject) {
+      var reader = new FileReader()
+      reader.onload = function() {
+        var view = new Uint8Array(reader.result)
+        resolve(Array.prototype.slice.call(view))
+      }
+      reader.onerror = function() {
+        reject(reader.error)
+      }
+      reader.readAsArrayBuffer(blob)
+    })
+  } else if ('FileReaderSync' in self) {
+    return new FileReaderSync().readAsArrayBuffer(blob)
+  } else {
+    throw new ReferenceError('FileReader is not defined')
+  }
+}
+
+function arrayBufferFromText(text) {
+  var buf = new ArrayBuffer(text.length)
+  var view = new Uint8Array(buf)
+
+  for (var i = 0; i < text.length; i++) {
+    view[i] = text.charCodeAt(i)
+  }
+  return buf
+}
+
+function readArrayBufferAsText(buf) {
+  var view = new Uint8Array(buf)
+  var chars = new Array(view.length)
+
+  for (var i = 0; i < view.length; i++) {
+    chars[i] = String.fromCharCode(view[i])
+  }
+  return chars.join('')
+}
+
+var preservedGlobals = {}
+var keepGlobals = ['fetch', 'Headers', 'Request', 'Response']
+var exercise = ['polyfill']
+
+// If native fetch implementation exists, save it and allow it to be replaced
+// by the polyfill. Native implementation will be exercised additionally.
+if (self.fetch) {
+  keepGlobals.forEach(function(name) {
+    preservedGlobals[name] = self[name]
+  })
+  self.fetch = undefined
+  exercise.push('native')
+}
+
+var slice = Array.prototype.slice
+
+function featureDependent(testOrSuite, condition) {
+  (condition ? testOrSuite : testOrSuite.skip).apply(this, slice.call(arguments, 2))
+}
+
+exercise.forEach(function(exerciseMode) {
+  suite(exerciseMode, function() {
+    if (exerciseMode === 'native') {
+      suiteSetup(function() {
+        keepGlobals.forEach(function(name) {
+          self[name] = preservedGlobals[name]
+        })
+      })
+    }
+
+    var nativeChrome = /Chrome\//.test(navigator.userAgent) && exerciseMode === 'native'
+    var polyfillFirefox = /Firefox\//.test(navigator.userAgent) && exerciseMode === 'polyfill'
+
+    // https://fetch.spec.whatwg.org/#concept-bodyinit-extract
+    function testBodyExtract(factory) {
+      suite('body extract', function() {
+        var expected = 'Hello World!'
+        var inputs = [['type USVString', expected]]
+        if (support.blob) {
+          inputs.push(['type Blob', new Blob([expected])])
+        }
+        if (support.arrayBuffer) {
+          inputs = inputs.concat([
+            ['type ArrayBuffer', arrayBufferFromText(expected)],
+            ['type TypedArray', new Uint8Array(arrayBufferFromText(expected))],
+            ['type DataView', new DataView(arrayBufferFromText(expected))],
+          ])
+        }
+
+        inputs.forEach(function(input) {
+          var typeLabel = input[0], body = input[1]
+
+          suite(typeLabel, function() {
+            featureDependent(test, support.blob, 'consume as blob', function() {
+              var r = factory(body)
+              return r.blob().then(readBlobAsText).then(function(text) {
+                assert.equal(text, expected)
+              })
+            })
+
+            test('consume as text', function() {
+              var r = factory(body)
+              return r.text().then(function(text) {
+                assert.equal(text, expected)
+              })
+            })
+
+            featureDependent(test, support.arrayBuffer, 'consume as array buffer', function() {
+              var r = factory(body)
+              return r.arrayBuffer().then(readArrayBufferAsText).then(function(text) {
+                assert.equal(text, expected)
+              })
+            })
+          })
+        })
+      })
+    }
+
+// https://fetch.spec.whatwg.org/#headers-class
+suite('Headers', function() {
+  test('constructor copies headers', function() {
+    var original = new Headers()
+    original.append('Accept', 'application/json')
+    original.append('Accept', 'text/plain')
+    original.append('Content-Type', 'text/html')
+
+    var headers = new Headers(original)
+    assert.equal(headers.get('Accept'), 'application/json,text/plain')
+    assert.equal(headers.get('Content-type'), 'text/html')
+  })
+  test('constructor works with arrays', function() {
+    var array = [
+      ['Content-Type', 'text/xml'],
+      ['Breaking-Bad', '<3']
+    ]
+    var headers = new Headers(array)
+
+    assert.equal(headers.get('Content-Type'), 'text/xml')
+    assert.equal(headers.get('Breaking-Bad'), '<3')
+  })
+  test('headers are case insensitive', function() {
+    var headers = new Headers({'Accept': 'application/json'})
+    assert.equal(headers.get('ACCEPT'), 'application/json')
+    assert.equal(headers.get('Accept'), 'application/json')
+    assert.equal(headers.get('accept'), 'application/json')
+  })
+  test('appends to existing', function() {
+    var headers = new Headers({'Accept': 'application/json'})
+    assert.isFalse(headers.has('Content-Type'))
+    headers.append('Content-Type', 'application/json')
+    assert.isTrue(headers.has('Content-Type'))
+    assert.equal(headers.get('Content-Type'), 'application/json')
+  })
+  test('appends values to existing header name', function() {
+    var headers = new Headers({'Accept': 'application/json'})
+    headers.append('Accept', 'text/plain')
+    assert.equal(headers.get('Accept'), 'application/json,text/plain')
+  })
+  test('sets header name and value', function() {
+    var headers = new Headers()
+    headers.set('Content-Type', 'application/json')
+    assert.equal(headers.get('Content-Type'), 'application/json')
+  })
+  test('returns null on no header found', function() {
+    var headers = new Headers()
+    assert.isNull(headers.get('Content-Type'))
+  })
+  test('has headers that are set', function() {
+    var headers = new Headers()
+    headers.set('Content-Type', 'application/json')
+    assert.isTrue(headers.has('Content-Type'))
+  })
+  test('deletes headers', function() {
+    var headers = new Headers()
+    headers.set('Content-Type', 'application/json')
+    assert.isTrue(headers.has('Content-Type'))
+    headers.delete('Content-Type')
+    assert.isFalse(headers.has('Content-Type'))
+    assert.isNull(headers.get('Content-Type'))
+  })
+  test('converts field name to string on set and get', function() {
+    var headers = new Headers()
+    headers.set(1, 'application/json')
+    assert.isTrue(headers.has('1'))
+    assert.equal(headers.get(1), 'application/json')
+  })
+  test('converts field value to string on set and get', function() {
+    var headers = new Headers()
+    headers.set('Content-Type', 1)
+    headers.set('X-CSRF-Token', undefined)
+    assert.equal(headers.get('Content-Type'), '1')
+    assert.equal(headers.get('X-CSRF-Token'), 'undefined')
+  })
+  test('throws TypeError on invalid character in field name', function() {
+    assert.throws(function() { new Headers({'<Accept>': 'application/json'}) }, TypeError)
+    assert.throws(function() { new Headers({'Accept:': 'application/json'}) }, TypeError)
+    assert.throws(function() {
+      var headers = new Headers()
+      headers.set({field: 'value'}, 'application/json')
+    }, TypeError)
+  })
+  test('is iterable with forEach', function() {
+    var headers = new Headers()
+    headers.append('Accept', 'application/json')
+    headers.append('Accept', 'text/plain')
+    headers.append('Content-Type', 'text/html')
+
+    var results = []
+    headers.forEach(function(value, key, object) {
+      results.push({value: value, key: key, object: object})
+    })
+
+    assert.equal(results.length, 2)
+    assert.deepEqual({key: 'accept', value: 'application/json,text/plain', object: headers}, results[0])
+    assert.deepEqual({key: 'content-type', value: 'text/html', object: headers}, results[1])
+  })
+  test('forEach accepts second thisArg argument', function() {
+    var headers = new Headers({'Accept': 'application/json'})
+    var thisArg = 42
+    headers.forEach(function() {
+      assert.equal(this, thisArg)
+    }, thisArg)
+  })
+  test('is iterable with keys', function() {
+    var headers = new Headers()
+    headers.append('Accept', 'application/json')
+    headers.append('Accept', 'text/plain')
+    headers.append('Content-Type', 'text/html')
+
+    var iterator = headers.keys()
+    assert.deepEqual({done: false, value: 'accept'}, iterator.next())
+    assert.deepEqual({done: false, value: 'content-type'}, iterator.next())
+    assert.deepEqual({done: true, value: undefined}, iterator.next())
+  })
+  test('is iterable with values', function() {
+    var headers = new Headers()
+    headers.append('Accept', 'application/json')
+    headers.append('Accept', 'text/plain')
+    headers.append('Content-Type', 'text/html')
+
+    var iterator = headers.values()
+    assert.deepEqual({done: false, value: 'application/json,text/plain'}, iterator.next())
+    assert.deepEqual({done: false, value: 'text/html'}, iterator.next())
+    assert.deepEqual({done: true, value: undefined}, iterator.next())
+  })
+  test('is iterable with entries', function() {
+    var headers = new Headers()
+    headers.append('Accept', 'application/json')
+    headers.append('Accept', 'text/plain')
+    headers.append('Content-Type', 'text/html')
+
+    var iterator = headers.entries()
+    assert.deepEqual({done: false, value: ['accept', 'application/json,text/plain']}, iterator.next())
+    assert.deepEqual({done: false, value: ['content-type', 'text/html']}, iterator.next())
+    assert.deepEqual({done: true, value: undefined}, iterator.next())
+  })
+ })
+
+// https://fetch.spec.whatwg.org/#request-class
+suite('Request', function() {
+  test('construct with string url', function() {
+    var request = new Request('https://fetch.spec.whatwg.org/')
+    assert.equal(request.url, 'https://fetch.spec.whatwg.org/')
+  })
+
+  featureDependent(test, support.url, 'construct with URL instance', function() {
+    var url = new URL('https://fetch.spec.whatwg.org/')
+    url.pathname = 'cors'
+    var request = new Request(url)
+    assert.equal(request.url, 'https://fetch.spec.whatwg.org/cors')
+  })
+
+  test('construct with non-Request object', function() {
+    var url = { toString: function() { return 'https://fetch.spec.whatwg.org/' } }
+    var request = new Request(url)
+    assert.equal(request.url, 'https://fetch.spec.whatwg.org/')
+  })
+
+  test('construct with Request', function() {
+    var request1 = new Request('https://fetch.spec.whatwg.org/', {
+      method: 'post',
+      body: 'I work out',
+      headers: {
+        accept: 'application/json',
+        'Content-Type': 'text/plain'
+      }
+    })
+    var request2 = new Request(request1)
+
+    return request2.text().then(function(body2) {
+      assert.equal(body2, 'I work out')
+      assert.equal(request2.method, 'POST')
+      assert.equal(request2.url, 'https://fetch.spec.whatwg.org/')
+      assert.equal(request2.headers.get('accept'), 'application/json')
+      assert.equal(request2.headers.get('content-type'), 'text/plain')
+
+      return request1.text().then(function() {
+        assert(false, 'original request body should have been consumed')
+      }, function(error) {
+        assert(error instanceof TypeError, 'expected TypeError for already read body')
+      })
+    })
+  })
+
+  test('construct with Request and override headers', function() {
+    var request1 = new Request('https://fetch.spec.whatwg.org/', {
+      method: 'post',
+      body: 'I work out',
+      headers: {
+        accept: 'application/json',
+        'X-Request-ID': '123'
+      }
+    })
+    var request2 = new Request(request1, {
+      headers: { 'x-test': '42' }
+    })
+
+    assert.equal(request2.headers.get('accept'), undefined)
+    assert.equal(request2.headers.get('x-request-id'), undefined)
+    assert.equal(request2.headers.get('x-test'), '42')
+  })
+
+  test('construct with Request and override body', function() {
+    var request1 = new Request('https://fetch.spec.whatwg.org/', {
+      method: 'post',
+      body: 'I work out',
+      headers: {
+        'Content-Type': 'text/plain'
+      }
+    })
+    var request2 = new Request(request1, {
+      body: '{"wiggles": 5}',
+      headers: { 'Content-Type': 'application/json' }
+    })
+
+    return request2.json().then(function(data) {
+      assert.equal(data.wiggles, 5)
+      assert.equal(request2.headers.get('content-type'), 'application/json')
+    })
+  })
+
+  featureDependent(test, !nativeChrome, 'construct with used Request body', function() {
+    var request1 = new Request('https://fetch.spec.whatwg.org/', {
+      method: 'post',
+      body: 'I work out'
+    })
+
+    return request1.text().then(function() {
+      assert.throws(function() {
+        new Request(request1)
+      }, TypeError)
+    })
+  })
+
+  test('GET should not have implicit Content-Type', function() {
+    var req = new Request('https://fetch.spec.whatwg.org/')
+    assert.equal(req.headers.get('content-type'), undefined)
+  })
+
+  test('POST with blank body should not have implicit Content-Type', function() {
+    var req = new Request('https://fetch.spec.whatwg.org/', {
+      method: 'post'
+    })
+    assert.equal(req.headers.get('content-type'), undefined)
+  })
+
+  test('construct with string body sets Content-Type header', function() {
+    var req = new Request('https://fetch.spec.whatwg.org/', {
+      method: 'post',
+      body: 'I work out'
+    })
+
+    assert.equal(req.headers.get('content-type'), 'text/plain;charset=UTF-8')
+  })
+
+  featureDependent(test, support.blob, 'construct with Blob body and type sets Content-Type header', function() {
+    var req = new Request('https://fetch.spec.whatwg.org/', {
+      method: 'post',
+      body: new Blob(['test'], { type: 'image/png' })
+    })
+
+    assert.equal(req.headers.get('content-type'), 'image/png')
+  })
+
+  test('construct with body and explicit header uses header', function() {
+    var req = new Request('https://fetch.spec.whatwg.org/', {
+      method: 'post',
+      headers: { 'Content-Type': 'image/png' },
+      body: 'I work out'
+    })
+
+    assert.equal(req.headers.get('content-type'), 'image/png')
+  })
+
+  featureDependent(test, support.blob, 'construct with Blob body and explicit Content-Type header', function() {
+    var req = new Request('https://fetch.spec.whatwg.org/', {
+      method: 'post',
+      headers: { 'Content-Type': 'image/png' },
+      body: new Blob(['test'], { type: 'text/plain' })
+    })
+
+    assert.equal(req.headers.get('content-type'), 'image/png')
+  })
+
+  featureDependent(test, support.searchParams, 'construct with URLSearchParams body sets Content-Type header', function() {
+    var req = new Request('https://fetch.spec.whatwg.org/', {
+      method: 'post',
+      body: new URLSearchParams('a=1&b=2')
+    })
+
+    assert.equal(req.headers.get('content-type'), 'application/x-www-form-urlencoded;charset=UTF-8')
+  })
+
+  featureDependent(test, support.searchParams, 'construct with URLSearchParams body and explicit Content-Type header', function() {
+    var req = new Request('https://fetch.spec.whatwg.org/', {
+      method: 'post',
+      headers: { 'Content-Type': 'image/png' },
+      body: new URLSearchParams('a=1&b=2')
+    })
+
+    assert.equal(req.headers.get('content-type'), 'image/png')
+  })
+
+  test('clone GET request', function() {
+    var req = new Request('https://fetch.spec.whatwg.org/', {
+      headers: {'content-type': 'text/plain'}
+    })
+    var clone = req.clone()
+
+    assert.equal(clone.url, req.url)
+    assert.equal(clone.method, 'GET')
+    assert.equal(clone.headers.get('content-type'), 'text/plain')
+    assert.notEqual(clone.headers, req.headers)
+    assert.isFalse(req.bodyUsed)
+  })
+
+  test('clone POST request', function() {
+    var req = new Request('https://fetch.spec.whatwg.org/', {
+      method: 'post',
+      headers: {'content-type': 'text/plain'},
+      body: 'I work out'
+    })
+    var clone = req.clone()
+
+    assert.equal(clone.method, 'POST')
+    assert.equal(clone.headers.get('content-type'), 'text/plain')
+    assert.notEqual(clone.headers, req.headers)
+    assert.equal(req.bodyUsed, false)
+
+    return Promise.all([clone.text(), req.clone().text()]).then(function(bodies) {
+      assert.deepEqual(bodies, ['I work out', 'I work out'])
+    })
+  })
+
+  featureDependent(test, !nativeChrome, 'clone with used Request body', function() {
+    var req = new Request('https://fetch.spec.whatwg.org/', {
+      method: 'post',
+      body: 'I work out'
+    })
+
+    return req.text().then(function() {
+      assert.throws(function() {
+        req.clone()
+      }, TypeError)
+    })
+  })
+
+  testBodyExtract(function(body) {
+    return new Request('', { method: 'POST', body: body })
+  })
+})
+
+// https://fetch.spec.whatwg.org/#response-class
+suite('Response', function() {
+  test('default status is 200 OK', function() {
+    var res = new Response()
+    assert.equal(res.status, 200)
+    assert.equal(res.statusText, 'OK')
+    assert.isTrue(res.ok)
+  })
+
+  testBodyExtract(function(body) {
+    return new Response(body)
+  })
+
+  test('creates Headers object from raw headers', function() {
+    var r = new Response('{"foo":"bar"}', {headers: {'content-type': 'application/json'}})
+    assert.equal(r.headers instanceof Headers, true)
+    return r.json().then(function(json){
+      assert.equal(json.foo, 'bar')
+      return json
+    })
+  })
+
+  test('always creates a new Headers instance', function() {
+    var headers = new Headers({ 'x-hello': 'world' })
+    var res = new Response('', {headers: headers})
+
+    assert.equal(res.headers.get('x-hello'), 'world')
+    assert.notEqual(res.headers, headers)
+  })
+
+  test('clone text response', function() {
+    var res = new Response('{"foo":"bar"}', {
+      headers: {'content-type': 'application/json'}
+    })
+    var clone = res.clone()
+
+    assert.notEqual(clone.headers, res.headers, 'headers were cloned')
+    assert.equal(clone.headers.get('content-type'), 'application/json')
+
+    return Promise.all([clone.json(), res.json()]).then(function(jsons){
+      assert.deepEqual(jsons[0], jsons[1], 'json of cloned object is the same as original')
+    })
+  })
+
+  featureDependent(test, support.blob, 'clone blob response', function() {
+    var req = new Request(new Blob(['test']))
+    req.clone()
+    assert.equal(req.bodyUsed, false)
+  })
+
+  test('error creates error Response', function() {
+    var r = Response.error()
+    assert(r instanceof Response)
+    assert.equal(r.status, 0)
+    assert.equal(r.statusText, '')
+    assert.equal(r.type, 'error')
+  })
+
+  test('redirect creates redirect Response', function() {
+    var r = Response.redirect('https://fetch.spec.whatwg.org/', 301)
+    assert(r instanceof Response)
+    assert.equal(r.status, 301)
+    assert.equal(r.headers.get('Location'), 'https://fetch.spec.whatwg.org/')
+  })
+
+  test('construct with string body sets Content-Type header', function() {
+    var r = new Response('I work out')
+    assert.equal(r.headers.get('content-type'), 'text/plain;charset=UTF-8')
+  })
+
+  featureDependent(test, support.blob, 'construct with Blob body and type sets Content-Type header', function() {
+    var r = new Response(new Blob(['test'], { type: 'text/plain' }))
+    assert.equal(r.headers.get('content-type'), 'text/plain')
+  })
+
+  test('construct with body and explicit header uses header', function() {
+    var r = new Response('I work out', {
+      headers: {
+        'Content-Type': 'text/plain'
+      },
+    })
+
+    assert.equal(r.headers.get('content-type'), 'text/plain')
+  })
+})
+
+// https://fetch.spec.whatwg.org/#body-mixin
+suite('Body mixin', function() {
+  featureDependent(suite, support.blob, 'arrayBuffer', function() {
+    test('resolves arrayBuffer promise', function() {
+      return fetch('/hello').then(function(response) {
+        return response.arrayBuffer()
+      }).then(function(buf) {
+        assert(buf instanceof ArrayBuffer, 'buf is an ArrayBuffer instance')
+        assert.equal(buf.byteLength, 2)
+      })
+    })
+
+    test('arrayBuffer handles binary data', function() {
+      return fetch('/binary').then(function(response) {
+        return response.arrayBuffer()
+      }).then(function(buf) {
+        assert(buf instanceof ArrayBuffer, 'buf is an ArrayBuffer instance')
+        assert.equal(buf.byteLength, 256, 'buf.byteLength is correct')
+        var view = new Uint8Array(buf)
+        for (var i = 0; i < 256; i++) {
+          assert.equal(view[i], i)
+        }
+      })
+    })
+
+    test('arrayBuffer handles utf-8 data', function() {
+      return fetch('/hello/utf8').then(function(response) {
+        return response.arrayBuffer()
+      }).then(function(buf) {
+        assert(buf instanceof ArrayBuffer, 'buf is an ArrayBuffer instance')
+        assert.equal(buf.byteLength, 5, 'buf.byteLength is correct')
+        var octets = Array.prototype.slice.call(new Uint8Array(buf))
+        assert.deepEqual(octets, [104, 101, 108, 108, 111])
+      })
+    })
+
+    test('arrayBuffer handles utf-16le data', function() {
+      return fetch('/hello/utf16le').then(function(response) {
+        return response.arrayBuffer()
+      }).then(function(buf) {
+        assert(buf instanceof ArrayBuffer, 'buf is an ArrayBuffer instance')
+        assert.equal(buf.byteLength, 10, 'buf.byteLength is correct')
+        var octets = Array.prototype.slice.call(new Uint8Array(buf))
+        assert.deepEqual(octets, [104, 0, 101, 0, 108, 0, 108, 0, 111, 0])
+      })
+    })
+
+    test('rejects arrayBuffer promise after body is consumed', function() {
+      return fetch('/hello').then(function(response) {
+        assert.equal(response.bodyUsed, false)
+        response.blob()
+        assert.equal(response.bodyUsed, true)
+        return response.arrayBuffer()
+      }).catch(function(error) {
+        assert(error instanceof TypeError, 'Promise rejected after body consumed')
+      })
+    })
+  })
+
+  featureDependent(suite, support.blob, 'blob', function() {
+    test('resolves blob promise', function() {
+      return fetch('/hello').then(function(response) {
+        return response.blob()
+      }).then(function(blob) {
+        assert(blob instanceof Blob, 'blob is a Blob instance')
+        assert.equal(blob.size, 2)
+      })
+    })
+
+    test('blob handles binary data', function() {
+      return fetch('/binary').then(function(response) {
+        return response.blob()
+      }).then(function(blob) {
+        assert(blob instanceof Blob, 'blob is a Blob instance')
+        assert.equal(blob.size, 256, 'blob.size is correct')
+      })
+    })
+
+    test('blob handles utf-8 data', function() {
+      return fetch('/hello/utf8').then(function(response) {
+        return response.blob()
+      }).then(readBlobAsBytes).then(function(octets) {
+        assert.equal(octets.length, 5, 'blob.size is correct')
+        assert.deepEqual(octets, [104, 101, 108, 108, 111])
+      })
+    })
+
+    test('blob handles utf-16le data', function() {
+      return fetch('/hello/utf16le').then(function(response) {
+        return response.blob()
+      }).then(readBlobAsBytes).then(function(octets) {
+        assert.equal(octets.length, 10, 'blob.size is correct')
+        assert.deepEqual(octets, [104, 0, 101, 0, 108, 0, 108, 0, 111, 0])
+      })
+    })
+
+    test('rejects blob promise after body is consumed', function() {
+      return fetch('/hello').then(function(response) {
+        assert(response.blob, 'Body does not implement blob')
+        assert.equal(response.bodyUsed, false)
+        response.text()
+        assert.equal(response.bodyUsed, true)
+        return response.blob()
+      }).catch(function(error) {
+        assert(error instanceof TypeError, 'Promise rejected after body consumed')
+      })
+    })
+  })
+
+  featureDependent(suite, support.formData, 'formData', function() {
+    test('post sets content-type header', function() {
+      return fetch('/request', {
+        method: 'post',
+        body: new FormData()
+      }).then(function(response) {
+        return response.json()
+      }).then(function(json) {
+        assert.equal(json.method, 'POST')
+        assert(/^multipart\/form-data;/.test(json.headers['content-type']))
+      })
+    })
+
+    featureDependent(test, !nativeChrome, 'rejects formData promise after body is consumed', function() {
+      return fetch('/json').then(function(response) {
+        assert(response.formData, 'Body does not implement formData')
+        response.formData()
+        return response.formData()
+      }).catch(function(error) {
+        if (error instanceof chai.AssertionError) {
+          throw error
+        } else {
+          assert(error instanceof TypeError, 'Promise rejected after body consumed')
+        }
+      })
+    })
+
+    featureDependent(test, !nativeChrome, 'parses form encoded response', function() {
+      return fetch('/form').then(function(response) {
+        return response.formData()
+      }).then(function(form) {
+        assert(form instanceof FormData, 'Parsed a FormData object')
+      })
+    })
+  })
+
+  suite('json', function() {
+    test('parses json response', function() {
+      return fetch('/json').then(function(response) {
+        return response.json()
+      }).then(function(json) {
+        assert.equal(json.name, 'Hubot')
+        assert.equal(json.login, 'hubot')
+      })
+    })
+
+    test('rejects json promise after body is consumed', function() {
+      return fetch('/json').then(function(response) {
+        assert(response.json, 'Body does not implement json')
+        assert.equal(response.bodyUsed, false)
+        response.text()
+        assert.equal(response.bodyUsed, true)
+        return response.json()
+      }).catch(function(error) {
+        assert(error instanceof TypeError, 'Promise rejected after body consumed')
+      })
+    })
+
+    featureDependent(test, !polyfillFirefox, 'handles json parse error', function() {
+      return fetch('/json-error').then(function(response) {
+        return response.json()
+      }).catch(function(error) {
+        assert(error instanceof Error, 'JSON exception is an Error instance')
+        assert(error.message, 'JSON exception has an error message')
+      })
+    })
+  })
+
+  suite('text', function() {
+    test('handles 204 No Content response', function() {
+      return fetch('/empty').then(function(response) {
+        assert.equal(response.status, 204)
+        return response.text()
+      }).then(function(body) {
+        assert.equal(body, '')
+      })
+    })
+
+    test('resolves text promise', function() {
+      return fetch('/hello').then(function(response) {
+        return response.text()
+      }).then(function(text) {
+        assert.equal(text, 'hi')
+      })
+    })
+
+    test('rejects text promise after body is consumed', function() {
+      return fetch('/hello').then(function(response) {
+        assert(response.text, 'Body does not implement text')
+        assert.equal(response.bodyUsed, false)
+        response.text()
+        assert.equal(response.bodyUsed, true)
+        return response.text()
+      }).catch(function(error) {
+        assert(error instanceof TypeError, 'Promise rejected after body consumed')
+      })
+    })
+  })
+})
+
+suite('fetch method', function() {
+  suite('promise resolution', function() {
+    test('resolves promise on 500 error', function() {
+      return fetch('/boom').then(function(response) {
+        assert.equal(response.status, 500)
+        assert.equal(response.ok, false)
+        return response.text()
+      }).then(function(body) {
+        assert.equal(body, 'boom')
+      })
+    })
+
+    test.skip('rejects promise for network error', function() {
+      return fetch('/error').then(function(response) {
+        assert(false, 'HTTP status ' + response.status + ' was treated as success')
+      }).catch(function(error) {
+        assert(error instanceof TypeError, 'Rejected with Error')
+      })
+    })
+
+    test('rejects when Request constructor throws', function() {
+      return fetch('/request', { method: 'GET', body: 'invalid' }).then(function() {
+        assert(false, 'Invalid Request init was accepted')
+      }).catch(function(error) {
+        assert(error instanceof TypeError, 'Rejected with Error')
+      })
+    })
+  })
+
+  suite('request', function() {
+    test('sends headers', function() {
+      return fetch('/request', {
+        headers: {
+          'Accept': 'application/json',
+          'X-Test': '42'
+        }
+      }).then(function(response) {
+        return response.json()
+      }).then(function(json) {
+        assert.equal(json.headers['accept'], 'application/json')
+        assert.equal(json.headers['x-test'], '42')
+      })
+    })
+
+    test('with Request as argument', function() {
+      var request = new Request('/request', {
+        headers: {
+          'Accept': 'application/json',
+          'X-Test': '42'
+        }
+      })
+
+      return fetch(request).then(function(response) {
+        return response.json()
+      }).then(function(json) {
+        assert.equal(json.headers['accept'], 'application/json')
+        assert.equal(json.headers['x-test'], '42')
+      })
+    })
+
+    test('reusing same Request multiple times', function() {
+      var request = new Request('/request', {
+        headers: {
+          'Accept': 'application/json',
+          'X-Test': '42'
+        }
+      })
+
+      var responses = []
+
+      return fetch(request).then(function(response) {
+        responses.push(response)
+        return fetch(request)
+      }).then(function(response) {
+        responses.push(response)
+        return fetch(request)
+      }).then(function(response) {
+        responses.push(response)
+        return Promise.all(responses.map(function(r) { return r.json() }))
+      }).then(function(jsons) {
+        jsons.forEach(function(json) {
+          assert.equal(json.headers['accept'], 'application/json')
+          assert.equal(json.headers['x-test'], '42')
+        })
+      })
+    })
+
+    featureDependent(suite, support.arrayBuffer, 'ArrayBuffer', function() {
+      test('ArrayBuffer body', function() {
+        return fetch('/request', {
+          method: 'post',
+          body: arrayBufferFromText('name=Hubot')
+        }).then(function(response) {
+          return response.json()
+        }).then(function(request) {
+          assert.equal(request.method, 'POST')
+          assert.equal(request.data, 'name=Hubot')
+        })
+      })
+
+      test('DataView body', function() {
+        return fetch('/request', {
+          method: 'post',
+          body: new DataView(arrayBufferFromText('name=Hubot'))
+        }).then(function(response) {
+          return response.json()
+        }).then(function(request) {
+          assert.equal(request.method, 'POST')
+          assert.equal(request.data, 'name=Hubot')
+        })
+      })
+
+      test('TypedArray body', function() {
+        return fetch('/request', {
+          method: 'post',
+          body: new Uint8Array(arrayBufferFromText('name=Hubot'))
+        }).then(function(response) {
+          return response.json()
+        }).then(function(request) {
+          assert.equal(request.method, 'POST')
+          assert.equal(request.data, 'name=Hubot')
+        })
+      })
+    })
+
+    featureDependent(test, support.searchParams, 'sends URLSearchParams body', function() {
+      return fetch('/request', {
+        method: 'post',
+        body: new URLSearchParams('a=1&b=2')
+      }).then(function(response) {
+        return response.json()
+      }).then(function(request) {
+        assert.equal(request.method, 'POST')
+        assert.equal(request.data, 'a=1&b=2')
+      })
+    })
+  })
+
+  suite('response', function() {
+    test('populates body', function() {
+      return fetch('/hello').then(function(response) {
+        assert.equal(response.status, 200)
+        assert.equal(response.ok, true)
+        return response.text()
+      }).then(function(body) {
+        assert.equal(body, 'hi')
+      })
+    })
+
+    test('parses headers', function() {
+      return fetch('/headers?' + new Date().getTime()).then(function(response) {
+        assert.equal(response.headers.get('Date'), 'Mon, 13 Oct 2014 21:02:27 GMT')
+        assert.equal(response.headers.get('Content-Type'), 'text/html; charset=utf-8')
+      })
+    })
+  })
+
+// https://fetch.spec.whatwg.org/#methods
+suite('HTTP methods', function() {
+  test('supports HTTP GET', function() {
+    return fetch('/request', {
+      method: 'get',
+    }).then(function(response) {
+      return response.json()
+    }).then(function(request) {
+      assert.equal(request.method, 'GET')
+      assert.equal(request.data, '')
+    })
+  })
+
+  test('GET with body throws TypeError', function() {
+    assert.throw(function() {
+      new Request('', {
+        method: 'get',
+        body: 'invalid'
+      })
+    }, TypeError)
+  })
+
+  test('HEAD with body throws TypeError', function() {
+    assert.throw(function() {
+      new Request('', {
+        method: 'head',
+        body: 'invalid'
+      })
+    }, TypeError)
+  })
+
+  test('supports HTTP POST', function() {
+    return fetch('/request', {
+      method: 'post',
+      body: 'name=Hubot'
+    }).then(function(response) {
+      return response.json()
+    }).then(function(request) {
+      assert.equal(request.method, 'POST')
+      assert.equal(request.data, 'name=Hubot')
+    })
+  })
+
+  test('supports HTTP PUT', function() {
+    return fetch('/request', {
+      method: 'put',
+      body: 'name=Hubot'
+    }).then(function(response) {
+      return response.json()
+    }).then(function(request) {
+      assert.equal(request.method, 'PUT')
+      assert.equal(request.data, 'name=Hubot')
+    })
+  })
+
+  featureDependent(test, support.patch, 'supports HTTP PATCH', function() {
+    return fetch('/request', {
+      method: 'PATCH',
+      body: 'name=Hubot'
+    }).then(function(response) {
+      return response.json()
+    }).then(function(request) {
+      assert.equal(request.method, 'PATCH')
+      assert.equal(request.data, 'name=Hubot')
+    })
+  })
+
+  test('supports HTTP DELETE', function() {
+    return fetch('/request', {
+      method: 'delete',
+    }).then(function(response) {
+      return response.json()
+    }).then(function(request) {
+      assert.equal(request.method, 'DELETE')
+      assert.equal(request.data, '')
+    })
+  })
+})
+
+// https://fetch.spec.whatwg.org/#atomic-http-redirect-handling
+suite('Atomic HTTP redirect handling', function() {
+  test('handles 301 redirect response', function() {
+    return fetch('/redirect/301').then(function(response) {
+      assert.equal(response.status, 200)
+      assert.equal(response.ok, true)
+      assert.match(response.url, /\/hello/)
+      return response.text()
+    }).then(function(body) {
+      assert.equal(body, 'hi')
+    })
+  })
+
+  test('handles 302 redirect response', function() {
+    return fetch('/redirect/302').then(function(response) {
+      assert.equal(response.status, 200)
+      assert.equal(response.ok, true)
+      assert.match(response.url, /\/hello/)
+      return response.text()
+    }).then(function(body) {
+      assert.equal(body, 'hi')
+    })
+  })
+
+  test('handles 303 redirect response', function() {
+    return fetch('/redirect/303').then(function(response) {
+      assert.equal(response.status, 200)
+      assert.equal(response.ok, true)
+      assert.match(response.url, /\/hello/)
+      return response.text()
+    }).then(function(body) {
+      assert.equal(body, 'hi')
+    })
+  })
+
+  test('handles 307 redirect response', function() {
+    return fetch('/redirect/307').then(function(response) {
+      assert.equal(response.status, 200)
+      assert.equal(response.ok, true)
+      assert.match(response.url, /\/hello/)
+      return response.text()
+    }).then(function(body) {
+      assert.equal(body, 'hi')
+    })
+  })
+
+  featureDependent(test, support.permanentRedirect, 'handles 308 redirect response', function() {
+    return fetch('/redirect/308').then(function(response) {
+      assert.equal(response.status, 200)
+      assert.equal(response.ok, true)
+      assert.match(response.url, /\/hello/)
+      return response.text()
+    }).then(function(body) {
+      assert.equal(body, 'hi')
+    })
+  })
+})
+
+// https://fetch.spec.whatwg.org/#concept-request-credentials-mode
+suite('credentials mode', function() {
+  setup(function() {
+    return fetch('/cookie?name=foo&value=reset', {credentials: 'same-origin'})
+  })
+
+  featureDependent(suite, exerciseMode === 'native', 'omit', function() {
+    test('request credentials defaults to omit', function() {
+      var request = new Request('')
+      assert.equal(request.credentials, 'omit')
+    })
+
+    test('does not accept cookies with implicit omit credentials', function() {
+      return fetch('/cookie?name=foo&value=bar').then(function() {
+        return fetch('/cookie?name=foo', {credentials: 'same-origin'})
+      }).then(function(response) {
+        return response.text()
+      }).then(function(data) {
+        assert.equal(data, 'reset')
+      })
+    })
+
+    test('does not accept cookies with omit credentials', function() {
+      return fetch('/cookie?name=foo&value=bar', {credentials: 'omit'}).then(function() {
+        return fetch('/cookie?name=foo', {credentials: 'same-origin'})
+      }).then(function(response) {
+        return response.text()
+      }).then(function(data) {
+        assert.equal(data, 'reset')
+      })
+    })
+
+    test('does not send cookies with implicit omit credentials', function() {
+      return fetch('/cookie?name=foo&value=bar', {credentials: 'same-origin'}).then(function() {
+        return fetch('/cookie?name=foo')
+      }).then(function(response) {
+        return response.text()
+      }).then(function(data) {
+        assert.equal(data, '')
+      })
+    })
+
+    test('does not send cookies with omit credentials', function() {
+      return fetch('/cookie?name=foo&value=bar').then(function() {
+        return fetch('/cookie?name=foo', {credentials: 'omit'})
+      }).then(function(response) {
+        return response.text()
+      }).then(function(data) {
+        assert.equal(data, '')
+      })
+    })
+  })
+
+  suite('same-origin', function() {
+    test('request credentials uses inits member', function() {
+      var request = new Request('', {credentials: 'same-origin'})
+      assert.equal(request.credentials, 'same-origin')
+    })
+
+    test('send cookies with same-origin credentials', function() {
+      return fetch('/cookie?name=foo&value=bar', {credentials: 'same-origin'}).then(function() {
+        return fetch('/cookie?name=foo', {credentials: 'same-origin'})
+      }).then(function(response) {
+        return response.text()
+      }).then(function(data) {
+        assert.equal(data, 'bar')
+      })
+    })
+  })
+
+  suite('include', function() {
+    test('send cookies with include credentials', function() {
+      return fetch('/cookie?name=foo&value=bar', {credentials: 'include'}).then(function() {
+        return fetch('/cookie?name=foo', {credentials: 'include'})
+      }).then(function(response) {
+        return response.text()
+      }).then(function(data) {
+        assert.equal(data, 'bar')
+      })
+    })
+  })
+})
+})
+
+  })
+})
diff --git a/test/worker.js b/test/worker.js
new file mode 100644
index 0000000..025d0dd
--- /dev/null
+++ b/test/worker.js
@@ -0,0 +1,38 @@
+importScripts('/node_modules/chai/chai.js')
+importScripts('/node_modules/mocha/mocha.js')
+
+mocha.setup('tdd')
+self.assert = chai.assert
+
+importScripts('/node_modules/promise-polyfill/promise.js')
+importScripts('/test/test.js')
+importScripts('/fetch.js')
+
+function title(test) {
+  return test.fullTitle().replace(/#/g, '');
+}
+
+function reporter(runner) {
+  runner.on('pending', function(test){
+    self.postMessage({name: 'pending', title: title(test)});
+  });
+
+  runner.on('pass', function(test){
+    self.postMessage({name: 'pass', title: title(test)});
+  });
+
+  runner.on('fail', function(test, err){
+    self.postMessage({
+      name: 'fail',
+      title: title(test),
+      message: err.message,
+      stack: err.stack
+    });
+  });
+
+  runner.on('end', function(){
+    self.postMessage({name: 'end'});
+  });
+}
+
+mocha.reporter(reporter).run()

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/libjs-fetch.git



More information about the Pkg-javascript-commits mailing list