[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