├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── Changelog.md ├── LICENSE ├── README.md ├── fetch-please.sublime-project ├── package.json ├── src ├── fetch-please.js └── helpers.js ├── test ├── abort.spec.js ├── delete.spec.js ├── fetch-please.spec.js ├── get.spec.js ├── helpers.spec.js ├── mocha.opts ├── post.spec.js ├── put.spec.js └── request.spec.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "ecmaFeatures": { 8 | "modules": true 9 | }, 10 | "parser": "babel-eslint", 11 | "rules": { 12 | "strict": 2, 13 | "comma-dangle": 1, 14 | "no-cond-assign": 2, 15 | "no-console": 0, 16 | "no-constant-condition": 1, 17 | "no-control-regex": 2, 18 | "no-debugger": 1, 19 | "no-dupe-args": 2, 20 | "no-dupe-keys": 2, 21 | "no-duplicate-case": 2, 22 | "no-empty-character-class": 2, 23 | "no-empty": 1, 24 | "no-ex-assign": 1, 25 | "no-extra-boolean-cast": 2, 26 | "no-extra-parens": 0, 27 | "no-extra-semi": 2, 28 | "no-func-assign": 2, 29 | "no-inner-declarations": 1, 30 | "no-invalid-regexp": 2, 31 | "no-irregular-whitespace": 2, 32 | "no-negated-in-lhs": 2, 33 | "no-obj-calls": 2, 34 | "no-regex-spaces": 2, 35 | "no-sparse-arrays": 2, 36 | "no-unreachable": 2, 37 | "use-isnan": 2, 38 | "valid-jsdoc": 0, 39 | "valid-typeof": 2, 40 | "no-unexpected-multiline": 1, 41 | 42 | "accessor-pairs": 1, 43 | "block-scoped-var": 2, 44 | "consistent-return": 1, 45 | "curly": 2, 46 | "default-case": 2, 47 | "dot-notation": 1, 48 | "dot-location": 0, 49 | "eqeqeq": 2, 50 | "guard-for-in": 0, 51 | "no-alert": 1, 52 | "no-caller": 2, 53 | "no-div-regex": 2, 54 | "no-else-return": 0, 55 | "no-eq-null": 2, 56 | "no-eval": 2, 57 | "no-extend-native": 2, 58 | "no-extra-bind": 2, 59 | "no-fallthrough": 0, 60 | "no-floating-decimal": 2, 61 | "no-implied-eval": 2, 62 | "no-iterator": 1, 63 | "no-labels": 2, 64 | "no-lone-blocks": 2, 65 | "no-loop-func": 1, 66 | "no-multi-spaces": 2, 67 | "no-multi-str": 1, 68 | "no-native-reassign": 2, 69 | "no-new-func": 2, 70 | "no-new-wrappers": 1, 71 | "no-new": 1, 72 | "no-octal": 2, 73 | "no-param-reassign": 0, 74 | "no-proto": 2, 75 | "no-redeclare": 2, 76 | "no-return-assign": 2, 77 | "no-script-url": 2, 78 | "no-self-compare": 2, 79 | "no-sequences": 1, 80 | "no-throw-literal": 2, 81 | "no-unused-expressions": 1, 82 | "no-void": 2, 83 | "no-with": 2, 84 | "radix": 2, 85 | "vars-on-top": 0, 86 | "wrap-iife": 1, 87 | "yoda": 1, 88 | 89 | "no-delete-var": 2, 90 | "no-shadow-restricted-names": 1, 91 | "no-shadow": 0, 92 | "no-undef": 1, 93 | "no-undefined": 0, 94 | "no-unused-vars": 1, 95 | "no-use-before-define": 2, 96 | 97 | "array-bracket-spacing": [1, "never"], 98 | "brace-style": [1, "1tbs", { 99 | "allowSingleLine": false 100 | }], 101 | "camelcase": 1, 102 | "comma-spacing": [1, { 103 | "before": false, 104 | "after": true 105 | }], 106 | "comma-style": [1, "last"], 107 | "computed-property-spacing": [1, "never"], 108 | "consistent-this": 0, 109 | "eol-last": 2, 110 | "func-names": 0, 111 | "func-style": [2, "declaration"], 112 | "indent": 2, 113 | "key-spacing": [2, { 114 | "beforeColon": false, 115 | "afterColon": true 116 | }], 117 | "linebreak-style": [2, "unix"], 118 | "max-nested-callbacks": [2, 4], 119 | "new-cap": 2, 120 | "new-parens": 2, 121 | "newline-after-var": 0, 122 | "no-continue": 0, 123 | "no-inline-comments": 0, 124 | "no-lonely-if": 1, 125 | "no-mixed-spaces-and-tabs": 2, 126 | "no-multiple-empty-lines": [1, { 127 | "max": 2 128 | }], 129 | "no-nested-ternary": 1, 130 | "no-new-object": 2, 131 | "no-spaced-func": 2, 132 | "no-trailing-spaces": 1, 133 | "no-underscore-dangle": 0, 134 | "no-unneeded-ternary": 2, 135 | "object-curly-spacing": [1, "never"], 136 | "one-var": 0, 137 | "operator-assignment": [2, "always"], 138 | "operator-linebreak": 0, 139 | "padded-blocks": [1, "never"], 140 | "quote-props": [1, "as-needed"], 141 | "quotes": [1, "single"], 142 | "semi-spacing": [2, { 143 | "before": false, 144 | "after": true 145 | }], 146 | "semi": 2, 147 | "sort-vars": 0, 148 | "keyword-spacing": [1, {"after": true}], 149 | "space-before-blocks": [1, "always"], 150 | "space-in-parens": [1, "never"], 151 | "space-infix-ops": 1, 152 | "spaced-comment": [1, "always"], 153 | "wrap-regex": 2, 154 | 155 | "no-this-before-super": 2, 156 | "no-var": 2, 157 | "object-shorthand": [2, "always"], 158 | "prefer-const": 0, 159 | 160 | "max-depth": [2, 4], 161 | "max-params": [2, 4], 162 | "no-bitwise": 2 163 | }, 164 | "globals": { 165 | "describe": true, 166 | "before": true, 167 | "after": true, 168 | "it": true 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .idea/ 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.eslintrc 2 | /.npmignore 3 | /.gitignore 4 | /.travis.yml 5 | /webpack.* 6 | /fetch-please.sublime-project 7 | /test/ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.12' 4 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | ### 2017-09-27 v0.4.3 2 | 3 | * Updated `babel-preset-es2015` to `babel-preset-env` 4 | 5 | ### 2016-10-24 v0.4.2 6 | 7 | * Implemeted support of `upload.progress` event 8 | 9 | ### 2016-10-24 v0.4.1 10 | 11 | * Implemented CORS (xhr.withCredentials) [#8](https://github.com/albburtsev/fetch-please/pull/8) 12 | 13 | ### 2016-10-22 v0.3.0 14 | 15 | * Implemeted support of `progress` event [#7](https://github.com/albburtsev/fetch-please/issues/7) 16 | 17 | ### 2016-10-09 v0.2.0 18 | 19 | * Don't ignore GET parameters with `false` value [#2](https://github.com/albburtsev/fetch-please/issues/2) 20 | 21 | ### 2015-03-10 v0.1.1 22 | 23 | * HTTP status in thrown error 24 | 25 | ### 2015-10-30 v0.1.0 26 | 27 | * Adds ability to set headers thru the callback, which will be called per each request 28 | * Bugfix: correct pipeline for non-JSON response 29 | * Bugfix: zero value can be set for GET parameter 30 | 31 | ### 2015-10-28 v0.0.2 32 | 33 | * Fixes wrong publushing, adds ```dist/``` folder into release 34 | 35 | ### 2015-10-27 v0.0.1 36 | 37 | First release. 38 | 39 | * Constructor ```FetchPlease``` takes two arguments: path and settings 40 | * Available settings: timeout, headers, handleResponse, handleJson 41 | * Methods ```get()``` and ```getRequest()``` 42 | * Methods ```post()``` and ```postRequest()``` 43 | * Methods ```put()``` and ```putRequest()``` 44 | * Methods ```delete()``` and ```deleteRequest()``` 45 | * Method ```abort()``` 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Alexander Burtsev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fetch-please.js [![Build Status](https://secure.travis-ci.org/albburtsev/fetch-please.png?branch=master)](https://travis-ci.org/albburtsev/fetch-please) 2 | 3 | HTTP-transport that supports [Promises](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise) and cancelable requests (XHR). Great for React! 4 | 5 | Moreover it's extra small (minified+gzipped less than 2.5Kb). 6 | 7 | ## Justification 8 | 9 | Handling requests with promises is really easy and convenient. But if you need to abort your request you can't do it with [fetch](https://fetch.spec.whatwg.org/). [Promises doesn't cancelable](https://esdiscuss.org/topic/cancelable-promises) outside of the constructor. 10 | 11 | This library provides simple API with cancelable requests and XHR under the hood. 12 | 13 | ## Install 14 | 15 | ```bash 16 | npm install fetch-please --save 17 | ``` 18 | 19 | ## Examples 20 | 21 | ```js 22 | import React from 'react'; 23 | import FetchPlease from 'fetch-please'; 24 | 25 | let api = new FetchPlease('/api/', { 26 | /* Settings here, see list of available settings below */ 27 | }); 28 | 29 | let SmartComponent = React.createClass({ 30 | getInitialState() { 31 | return { 32 | loading: false, 33 | error: null, 34 | users: [] 35 | }; 36 | }, 37 | 38 | /** 39 | * Fetch state (list of users) for this component 40 | */ 41 | componentWillMount() { 42 | api 43 | .get('users/', { 44 | limit: 20, 45 | offset: 10 46 | }) 47 | .then((json) => { 48 | // List of users successfully received 49 | this.setState({ 50 | loading: false, 51 | users: json.users 52 | }); 53 | }) 54 | .catch((error) => { 55 | // Oops, something goes wrong 56 | this.setState({ 57 | error, 58 | loading: false 59 | }); 60 | }); 61 | 62 | // Wait response 63 | this.setState({loading: true}); 64 | }, 65 | 66 | /** 67 | * Our component was unexpectedly unmounted 68 | * Application doesn't need in requested data 69 | */ 70 | componentWillUnmount() { 71 | // Abort all opened requests for this instance of FetchPlease 72 | api.abort(); 73 | } 74 | }); 75 | ``` 76 | 77 | If you want to abort individual request, you can do this with special API: 78 | 79 | ```js 80 | // ... same part in the previous example 81 | 82 | componentWillMount() { 83 | // It's another API, that returns XHR object as a Promise instance 84 | let {xhr, promise} = api.getRequest('users/'); 85 | 86 | promise 87 | .then((json) => { 88 | /* Save users in state */ 89 | }) 90 | .catch((error) => { 91 | /* Save error in state */ 92 | }); 93 | 94 | // Save necessary request 95 | this.xhr = xhr; 96 | this.setState({loading: true}); 97 | }, 98 | 99 | componentWillUnmount() { 100 | // Abort individual requst with saved XHR object 101 | this.xhr.abort(); 102 | } 103 | 104 | // ... same part in the previous example 105 | ``` 106 | 107 | ## Requirements 108 | 109 | ES5 compatible besides Promises. Use [polyfill for ES6 Promises](https://github.com/jakearchibald/es6-promise). 110 | 111 | If you need ES3 compatible version use polyfills for ```JSON```, ```Object.keys```, ```Array.prototype.indexOf```, ```Array.prototype.map```, ```Array.prototype.reduce```, ```Array.prototype.filter```, ```Array.prototype.forEach```. 112 | 113 | ## API 114 | 115 | ### Constructor 116 | 117 | ``` 118 | FetchPlease([String path], [Object settings]); 119 | ``` 120 | 121 | For example: 122 | 123 | ```js 124 | let api = new FetchPlease('/api/', { 125 | timeout: 3000, // 3s 126 | headers: { 127 | 'Content-Type': 'application/json', 128 | 'X-Custom-Header': 'custom' 129 | } 130 | }); 131 | ``` 132 | 133 | #### Settings 134 | 135 | `timeout = 0` 136 | 137 | A number of milliseconds a request can take before automatically being terminated. The value of 0 means there is no timeout. 138 | 139 | `headers = {}` 140 | 141 | An object with HTTP headers for all requests. 142 | 143 | `handleResponse` 144 | 145 | Takes XHR object as a single argument and returns it if response looks like acceptable. 146 | 147 | Example of custom response handler: 148 | 149 | ```js 150 | let api = new FetchPlease('/api/', { 151 | handleResponse(xhr) { 152 | if (xhr.status !== 200) { 153 | throw new Error('Nooooooo!'); 154 | } 155 | 156 | return xhr; 157 | } 158 | }); 159 | ``` 160 | 161 | `handleJson` 162 | 163 | Takes XHR object as a single argument and returns object corresponding to the given JSON in responseText. Invokes if response header ```Content-Type: application/json``` exists. 164 | 165 | ### Methods 166 | 167 | #### get() and getRequest() 168 | 169 | ``` 170 | Promise get(String url, [Object params,] [Object settings]) 171 | ``` 172 | 173 | Sends GET request with optional parameters and same optional settings (see above for constructor). Returns an instance of ```Promise```. 174 | 175 | ``` 176 | Object getRequests(String url, [Object params,] [Object settings]) 177 | ``` 178 | 179 | Sends GET request. Returns object with two properties: ```xhr``` (instance of ```XMLHttpRequest```) and ```promise``` (instance of ```Promise```). 180 | 181 | Example: 182 | 183 | ```js 184 | let api = new FetchPlease('/api/'); 185 | api 186 | .get('users', {limit: 10, offset: 50}) // sends GET request on /api/users?limit=10&offset=50 187 | .then((data) => { 188 | console.log(data); 189 | }) 190 | .catch((error) => { 191 | console.error(error); 192 | }); 193 | ``` 194 | 195 | #### post() and postRequest() 196 | 197 | ``` 198 | Promise post(String url, [Object data,] [Object settings]) 199 | ``` 200 | 201 | ``` 202 | Object postRequest(String url, [Object data,] [Object settings]) 203 | ``` 204 | 205 | Sends data as a body of HTTP request. If a data object is an instance of ```Blob``` or ```FormData``` or ```String```, it will be sent without transformation. Otherwise, data will be sent as serialized JSON string. 206 | 207 | Example: 208 | 209 | ```js 210 | let api = new FetchPlease('/api/'); 211 | api 212 | .post('users', {name: 'Mary', surname: 'Brown'}) // sends POST request on /api/users 213 | .then((data) => { 214 | console.log(data); 215 | }) 216 | .catch((error) => { 217 | console.error(error); 218 | }); 219 | ``` 220 | 221 | #### put() and putRequest() 222 | 223 | ``` 224 | Promise put(String url, [Object data,] [Object settings]) 225 | ``` 226 | 227 | ``` 228 | Object putRequest(String url, [Object data,] [Object settings]) 229 | ``` 230 | 231 | As well as ```post()``` and ```postRequest()```. 232 | 233 | #### delete() and deleteRequest() 234 | 235 | ``` 236 | Promise delete(String url, [Object params,] [Object settings]) 237 | ``` 238 | 239 | ``` 240 | Object deleteRequests(String url, [Object params,] [Object settings]) 241 | ``` 242 | 243 | As well as ```get()``` and ```getRequest()```. 244 | 245 | #### abort() 246 | 247 | Aborts all opened requests for appropriate FetchPlease instance. Example: 248 | 249 | ```js 250 | let api = new FetchPlease('/api/'); 251 | 252 | api 253 | .get('users') 254 | .catch((error) => { 255 | console.error(error); // Error['Resource has been aborted'] 256 | }); 257 | 258 | api.abort(); 259 | ``` 260 | 261 | -------------------------------------------------------------------------------- /fetch-please.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": ".", 5 | "folder_exclude_patterns": [ 6 | ".git", 7 | "node_modules", 8 | "dist" 9 | ] 10 | } 11 | ], 12 | "settings": { 13 | "tab_size": 4, 14 | "translate_tabs_to_spaces": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fetch-please", 3 | "version": "0.4.3", 4 | "description": "HTTP-transport that supports Promises and cancelable requests (XHR)", 5 | "main": "dist/fetch-please.js", 6 | "scripts": { 7 | "build:umd": "webpack src/fetch-please.js dist/fetch-please.js --config webpack.config.js", 8 | "build:umd:min": "NODE_ENV=production webpack src/fetch-please.js dist/fetch-please.min.js --config webpack.config.js", 9 | "build": "npm run build:umd && npm run build:umd:min", 10 | "build:watch": "npm run build:umd -- --watch", 11 | "prepublish": "npm run build", 12 | "lint": "eslint src/ test/", 13 | "test": "mocha test", 14 | "test:watch": "npm test -- --watch" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/albburtsev/fetch-please.git" 19 | }, 20 | "keywords": [ 21 | "fetch", 22 | "XHR", 23 | "XMLHttpRequest", 24 | "HTTP", 25 | "get", 26 | "post", 27 | "put", 28 | "delete", 29 | "abort", 30 | "cancel", 31 | "cancelable", 32 | "requests", 33 | "promise", 34 | "REST API", 35 | "React" 36 | ], 37 | "author": { 38 | "name": "Alexander Burtsev", 39 | "url": "https://github.com/albburtsev" 40 | }, 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/albburtsev/fetch-please/issues" 44 | }, 45 | "homepage": "https://github.com/albburtsev/fetch-please#readme", 46 | "devDependencies": { 47 | "babel-core": "^6.8.0", 48 | "babel-eslint": "^6.0.4", 49 | "babel-loader": "^6.2.4", 50 | "babel-preset-env": "^1.6.0", 51 | "chai": "^3.4.0", 52 | "eslint": "^2.11.1", 53 | "eslint-loader": "^1.3.0", 54 | "eslint-plugin-babel": "^3.2.0", 55 | "eslint-plugin-react": "^5.1.1", 56 | "lodash": "^3.10.1", 57 | "mocha": "^2.3.3", 58 | "sinon": "^1.17.2", 59 | "webpack": "^1.12.2" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/fetch-please.js: -------------------------------------------------------------------------------- 1 | import {assign, joinParams} from './helpers'; 2 | 3 | export const HTTP_METHOD_GET = 'GET'; 4 | export const HTTP_METHOD_PUT = 'PUT'; 5 | export const HTTP_METHOD_POST = 'POST'; 6 | export const HTTP_METHOD_DELETE = 'DELETE'; 7 | export const ALLOWED_METHODS = [ 8 | HTTP_METHOD_GET, 9 | HTTP_METHOD_PUT, 10 | HTTP_METHOD_POST, 11 | HTTP_METHOD_DELETE 12 | ]; 13 | 14 | export const CONTENT_TYPE_JSON = 'application/json'; 15 | export const MIN_SUCCESSFUL_HTTP_CODE = 200; 16 | export const MAX_SUCCESSFUL_HTTP_CODE = 299; 17 | 18 | export const ERROR_XHR_NOT_FOUND = 'Constructor XMLHttpRequest not found'; 19 | export const ERROR_PROMISE_NOT_FOUND = 'Constructor Promise not found'; 20 | export const ERROR_UNKNOWN_HTTP_METHOD = 'Unknown HTTP method'; 21 | export const ERROR_UNACCEPTABLE_HTTP_CODE = 'Unacceptable HTTP code'; 22 | export const ERROR_INVALID_DATA = 'Invalid data for sending'; 23 | export const ERROR_JSON_PARSE = 'Invalid JSON'; 24 | 25 | export const ERROR_CONNECTION_TIMEOUT = 'Connection timeout'; 26 | export const ERROR_RESOURCE_ABORTED = 'Resource has been aborted'; 27 | export const ERROR_RESOURCE_FAILED = 'Resource failed to load'; 28 | 29 | /** 30 | * @class HTTP-transport based on XHR 31 | * @property {String} path Common path 32 | * @property {Number} timeout Timeout (in milliseconds) 33 | * @property {Object} headers Common headers 34 | * @property {Array} opened List of opened requests 35 | * @property {Boolean} cors Use `withCredentials` 36 | * @property {XMLHttpRequest} XMLHttpRequest XHR interface 37 | * @property {Boolean} cors ```true``` if supported Cross-Origin Resource Sharing 38 | * @see https://xhr.spec.whatwg.org/ 39 | * @see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest 40 | * @see http://www.html5rocks.com/en/tutorials/cors/ 41 | */ 42 | class FetchPlease { 43 | /** 44 | * @param {String} path Common path for all requests 45 | * @param {Object} [settings] Object with settings 46 | * @param {Number} [settings.timeout = 0] 47 | * @param {Object} [settings.XMLHttpRequest = global.XMLHttpRequest] 48 | * @param {Object} [settings.cors = false] 49 | * @param {Function} [settings.onProgress] Handler for xhr.onprogress 50 | * @param {Function} [settings.onProgressUpload] Handler for xhr.upload.onprogress 51 | * @param {Object|Function} [settings.headers = {}] 52 | */ 53 | constructor(path = '', settings = {}) { 54 | this.path = path; 55 | this.opened = []; 56 | 57 | assign(this, { 58 | timeout: 0, 59 | headers: {}, 60 | cors: false, 61 | XMLHttpRequest: global.XMLHttpRequest 62 | }, settings); 63 | 64 | let {XMLHttpRequest} = this; 65 | if (XMLHttpRequest) { 66 | this.cors = settings.cors && 'withCredentials' in (new XMLHttpRequest()); 67 | } 68 | } 69 | 70 | /** 71 | * Creates XHR instance and Promise instance 72 | * @param {String} method HTTP method 73 | * @param {String} path 74 | * @param {Object} data 75 | * @param {Object} settings 76 | * @return {Object} 77 | */ 78 | request(method, path, data = null, settings = null) { 79 | if (!this.XMLHttpRequest) { 80 | throw new Error(ERROR_XHR_NOT_FOUND); 81 | } 82 | 83 | if (!global.Promise) { 84 | throw new Error(ERROR_PROMISE_NOT_FOUND); 85 | } 86 | 87 | if (ALLOWED_METHODS.indexOf(method) === -1) { 88 | throw new Error(ERROR_UNKNOWN_HTTP_METHOD); 89 | } 90 | 91 | let xhr = new this.XMLHttpRequest(), 92 | promise = new Promise(function (resolve, reject) { 93 | xhr.addEventListener('error', () => reject(ERROR_RESOURCE_FAILED)); 94 | xhr.addEventListener('abort', () => reject(ERROR_RESOURCE_ABORTED)); 95 | xhr.addEventListener('timeout', () => reject(ERROR_CONNECTION_TIMEOUT)); 96 | xhr.addEventListener('load', () => { 97 | if (xhr.status) { 98 | resolve(xhr); 99 | } 100 | }); 101 | }); 102 | 103 | settings = settings || {}; 104 | 105 | let handleJson = settings.handleJson || this.handleJson, 106 | handleError = settings.handleError || this.handleError, 107 | handleResponse = settings.handleResponse || this.handleResponse; 108 | 109 | promise = promise 110 | // Remove request from list of opened requests 111 | .then( 112 | () => this.close(xhr), 113 | (reason) => { 114 | this.close(xhr); 115 | throw new Error(reason); 116 | } 117 | ) 118 | // Handle response 119 | .then(handleResponse) 120 | // Handle JSON in response 121 | .then(handleJson) 122 | // Handle errors 123 | .catch(handleError); 124 | 125 | // Add optinal `onProgress` handler 126 | if (settings.onProgress) { 127 | xhr.addEventListener('progress', settings.onProgress); 128 | } 129 | 130 | // Add optinal `onProgressUpload` handler 131 | if (settings.onProgressUpload) { 132 | xhr.upload.addEventListener('progress', settings.onProgressUpload); 133 | } 134 | 135 | // Form URL without normalizing and open request 136 | let url = this.path + path; 137 | xhr.open(method, url); 138 | 139 | // Set headers 140 | // Order of method's calls is important 141 | // You must call setRequestHeader() after open(), but before send() 142 | // @see: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest#setRequestHeader() 143 | let headers = assign( 144 | {}, 145 | typeof this.headers === 'function' ? this.headers() : this.headers, 146 | settings.headers 147 | ); 148 | this.setHeaders(xhr, headers); 149 | 150 | // Set timeout (before send() too) 151 | // @see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Synchronous_and_Asynchronous_Requests#Example_using_a_timeout 152 | let timeout = this.timeout || settings.timeout || 0; 153 | xhr.timeout = timeout; 154 | 155 | // Serialize data and send request 156 | data = this.serialize(data); 157 | 158 | if (this.cors) { 159 | xhr.withCredentials = true; 160 | } 161 | 162 | xhr.send(data); 163 | 164 | // Add request into list of opened requests 165 | this.add(xhr); 166 | 167 | return {xhr, promise}; 168 | } 169 | 170 | /** 171 | * Serializes data for request 172 | * @param {*} data 173 | * @return {String|FormData|Blob} 174 | */ 175 | serialize(data) { 176 | // Do nothing with FormData instance 177 | if (global.FormData && data instanceof FormData) { 178 | return data; 179 | } 180 | 181 | // Do nothing with Blob instance 182 | if (global.Blob && data instanceof Blob) { 183 | return data; 184 | } 185 | 186 | // Do nothing with string or null 187 | if (data === null || typeof data === 'string') { 188 | return data; 189 | } 190 | 191 | try { 192 | // Serialize other data as JSON 193 | return JSON.stringify(data); 194 | } catch (e) { 195 | throw new Error(ERROR_INVALID_DATA); 196 | } 197 | } 198 | 199 | /** 200 | * Iterates given headers and set them for XHR object 201 | * @param {XMLHttpRequest} xhr 202 | * @param {Object} headers 203 | * @return {XMLHttpRequest} 204 | */ 205 | setHeaders(xhr, headers = {}) { 206 | return Object.keys(headers).reduce((xhr, header) => { 207 | let value = headers[header]; 208 | 209 | if (value !== false && value !== null && value !== undefined) { 210 | xhr.setRequestHeader(header, value); 211 | } 212 | 213 | return xhr; 214 | }, xhr); 215 | } 216 | 217 | /** 218 | * Aborts all opened requests 219 | */ 220 | abort() { 221 | this.opened.forEach((xhr) => xhr.abort()); 222 | } 223 | 224 | /** 225 | * Adds request to list of opened requests 226 | * @param {XMLHttpRequest} xhr 227 | */ 228 | add(xhr) { 229 | this.opened.push(xhr); 230 | } 231 | 232 | /** 233 | * Deletes request from list of opened requests 234 | * @param {XMLHttpRequest} xhr 235 | * @return {XMLHtpRequest} 236 | */ 237 | close(xhr) { 238 | let idx = this.opened.indexOf(xhr); 239 | if (idx !== -1) { 240 | this.opened.splice(idx, 1); 241 | } 242 | return xhr; 243 | } 244 | 245 | /** 246 | * Sends GET request 247 | * @param {String} url 248 | * @param {Object} [params] 249 | * @param {Object} [settings] 250 | * @return {Promise} 251 | */ 252 | get(url, params, settings) { 253 | let {promise} = this.getRequest(url, params, settings); 254 | return promise; 255 | } 256 | 257 | /** 258 | * Sends PUT request 259 | * @param {String} url 260 | * @param {Object} data 261 | * @param {Object} [settings] 262 | * @return {Promise} 263 | */ 264 | put(url, data, settings) { 265 | let {promise} = this.putRequest(url, data, settings); 266 | return promise; 267 | } 268 | 269 | /** 270 | * Sends POST request 271 | * @param {String} url 272 | * @param {Object} data 273 | * @param {Object} [settings] 274 | * @return {Promise} 275 | */ 276 | post(url, data, settings) { 277 | let {promise} = this.postRequest(url, data, settings); 278 | return promise; 279 | } 280 | 281 | /** 282 | * Sends DELETE request 283 | * @param {String} url 284 | * @param {Object} [params] 285 | * @param {Object} [settings] 286 | * @return {Promise} 287 | */ 288 | delete(url, params, settings) { 289 | let {promise} = this.deleteRequest(url, params, settings); 290 | return promise; 291 | } 292 | 293 | /** 294 | * Sends same GET request but returns object with instances of XHR and Promise 295 | * @param {String} url 296 | * @param {Object} [params] 297 | * @param {Object} [settings] 298 | * @return {Object} 299 | */ 300 | getRequest(url, params = null, settings = null) { 301 | url = joinParams(url, params); 302 | return this.request(HTTP_METHOD_GET, url, null, settings); 303 | } 304 | 305 | /** 306 | * Sends same PUT request but returns object with instances of XHR and Promise 307 | * @param {String} url 308 | * @param {Object} data 309 | * @param {Object} [settings] 310 | * @return {Object} 311 | */ 312 | putRequest(url, data, settings = null) { 313 | return this.request(HTTP_METHOD_PUT, url, data, settings); 314 | } 315 | 316 | /** 317 | * Sends same POST request but returns object with instances of XHR and Promise 318 | * @param {String} url 319 | * @param {Object} data 320 | * @param {Object} [settings] 321 | * @return {Object} 322 | */ 323 | postRequest(url, data, settings = null) { 324 | return this.request(HTTP_METHOD_POST, url, data, settings); 325 | } 326 | 327 | /** 328 | * Sends same DELETE request but returns object with instances of XHR and Promise 329 | * @param {String} url 330 | * @param {Object} [params] 331 | * @param {Object} [settings] 332 | * @return {Object} 333 | */ 334 | deleteRequest(url, params = null, settings = null) { 335 | url = joinParams(url, params); 336 | return this.request(HTTP_METHOD_DELETE, url, null, settings); 337 | } 338 | 339 | /** 340 | * Handles JSON in response 341 | * @param {XMLHttpRequest} xhr 342 | * @param {String} xhr.responseText 343 | * @return {Object} 344 | */ 345 | handleJson(xhr) { 346 | let {responseText} = xhr, 347 | contentType = xhr.getResponseHeader('Content-Type') || ''; 348 | 349 | contentType = contentType.toLowerCase(); 350 | 351 | // If header Content-Type has value "application/json" then parse text as JSON 352 | if (contentType.indexOf(CONTENT_TYPE_JSON) !== -1) { 353 | try { 354 | return JSON.parse(responseText); 355 | } catch (e) { 356 | throw new Error(ERROR_JSON_PARSE); 357 | } 358 | } 359 | 360 | // Just return given xhr 361 | return xhr; 362 | } 363 | 364 | /** 365 | * Handles XHR response 366 | * @param {XMLHttpRequest} xhr 367 | * @param {Number} xhr.status 368 | * @see https://xhr.spec.whatwg.org/#interface-xmlhttprequest 369 | * @return {XMLHttpRequest} 370 | */ 371 | handleResponse(xhr) { 372 | let {status} = xhr; 373 | 374 | // If status < 200 or status >= 200 then return error 375 | if (status < MIN_SUCCESSFUL_HTTP_CODE || status > MAX_SUCCESSFUL_HTTP_CODE) { 376 | let error = new Error(ERROR_UNACCEPTABLE_HTTP_CODE); 377 | error.statusCode = status; 378 | throw error; 379 | } 380 | 381 | // Just return given xhr 382 | return xhr; 383 | } 384 | 385 | /** 386 | * Handles any occured errors 387 | * @param {Error} error 388 | * @return {Error} 389 | */ 390 | handleError(error) { 391 | // Really do nothing 392 | throw error; 393 | } 394 | } 395 | 396 | export default FetchPlease; 397 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Naive polyfill for Object.assign() 3 | * Why I did it? Because even modular lodash adds too much code in final build 4 | * @param {Object} target 5 | * @return {Object} 6 | */ 7 | export function assign(target) { 8 | for (let i = 1; i < arguments.length; i++) { 9 | let obj = Object(arguments[i]), 10 | keys = Object.keys(obj); 11 | 12 | for (let j = 0; j < keys.length; j++) { 13 | let key = keys[j]; 14 | 15 | if (!obj.hasOwnProperty(key)) { 16 | continue; 17 | } 18 | 19 | target[key] = obj[key]; 20 | } 21 | } 22 | 23 | return target; 24 | } 25 | 26 | /** 27 | * Returns URL with query string 28 | * @param {String} url 29 | * @param {Object} params 30 | * @return {String} 31 | */ 32 | export function joinParams(url, params = null) { 33 | if (!params) { 34 | return url; 35 | } 36 | 37 | let queryString = Object.keys(params) 38 | .filter((key) => { 39 | let value = params[key]; 40 | return value !== null && value !== undefined; 41 | }) 42 | .map((key) => { 43 | let value = params[key].toString(); 44 | 45 | key = encodeURIComponent(key); 46 | value = encodeURIComponent(value); 47 | 48 | return `${key}=${value}`; 49 | }) 50 | .join('&'); 51 | 52 | if (!queryString) { 53 | return url; 54 | } 55 | 56 | // Trim trailing "?" 57 | url = url.replace(/\?+$/, ''); 58 | 59 | return url + 60 | (url.indexOf('?') === -1 ? '?' : '&') + 61 | queryString; 62 | } 63 | -------------------------------------------------------------------------------- /test/abort.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import {expect} from 'chai'; 3 | import FetchPlease, {ERROR_RESOURCE_ABORTED} from '../src/fetch-please'; 4 | 5 | let XMLHttpRequest = sinon.useFakeXMLHttpRequest(); 6 | 7 | describe('Method abort()', () => { 8 | let api = new FetchPlease('/api/', {XMLHttpRequest}); 9 | 10 | it('exists', function() { 11 | expect(api.abort).to.be.a('function'); 12 | }); 13 | 14 | it('aborted all opened requests', function() { 15 | let first = api.request('GET', '/'), 16 | second = api.request('GET', '/'); 17 | 18 | expect(api.opened.length).to.equal(2); 19 | 20 | api.abort(); 21 | 22 | return first.promise 23 | .catch(() => second.promise) 24 | .catch((error) => { 25 | expect(api.opened.length).to.equal(0); 26 | expect(error).to.be.an.instanceOf(Error); 27 | expect(error.message).to.equal(ERROR_RESOURCE_ABORTED); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/delete.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import {expect} from 'chai'; 3 | import FetchPlease from '../src/fetch-please'; 4 | 5 | let XMLHttpRequest = sinon.useFakeXMLHttpRequest(); 6 | 7 | describe('Method deleteRequest()', () => { 8 | before(function() { 9 | this.requests = []; 10 | this.api = new FetchPlease('/api/', {XMLHttpRequest}); 11 | 12 | XMLHttpRequest.onCreate = (xhr) => { 13 | this.requests.push(xhr); 14 | }; 15 | }); 16 | 17 | it('exists', function() { 18 | expect(this.api.deleteRequest).to.be.a('function'); 19 | }); 20 | 21 | it('sends request', function() { 22 | expect(this.requests.length).to.equal(0); 23 | 24 | let {xhr, promise} = this.api.deleteRequest('users', {id: 1}); 25 | 26 | expect(xhr).to.be.an.instanceof(XMLHttpRequest); 27 | expect(xhr.url).be.equal('/api/users?id=1'); 28 | expect(xhr.method).be.equal('DELETE'); 29 | expect(promise).to.be.an.instanceof(Promise); 30 | expect(this.requests.length).to.equal(1); 31 | 32 | xhr.respond(200, {'content-type': 'application/json'}, '{"result": true}'); 33 | 34 | return promise.then((data) => { 35 | expect(data).to.deep.equal({result: true}); 36 | }); 37 | }); 38 | 39 | after(function() { 40 | this.api = null; 41 | this.requests = []; 42 | XMLHttpRequest.onCreate = null; 43 | }); 44 | }); 45 | 46 | describe('Method delete()', () => { 47 | before(function() { 48 | this.requests = []; 49 | this.api = new FetchPlease('/api/', {XMLHttpRequest}); 50 | 51 | XMLHttpRequest.onCreate = (xhr) => { 52 | this.requests.push(xhr); 53 | }; 54 | }); 55 | 56 | it('exists', function() { 57 | expect(this.api.delete).to.be.a('function'); 58 | }); 59 | 60 | it('sends request', function() { 61 | expect(this.requests.length).to.equal(0); 62 | 63 | let promise = this.api.delete('users', {id: 1}); 64 | 65 | expect(promise).to.be.an.instanceof(Promise); 66 | expect(this.requests.length).to.equal(1); 67 | 68 | this.requests[0].respond(200, {'content-type': 'application/json'}, '{"result": true}'); 69 | 70 | return promise.then((data) => { 71 | expect(data).to.deep.equal({result: true}); 72 | }); 73 | }); 74 | 75 | after(function() { 76 | this.api = null; 77 | this.requests = []; 78 | XMLHttpRequest.onCreate = null; 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/fetch-please.spec.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import FetchPlease from '../src/fetch-please'; 3 | 4 | describe('Class FetchPlease', () => { 5 | it('exist', () => { 6 | expect(FetchPlease).to.be.an.instanceof(Function); 7 | }); 8 | }); 9 | 10 | describe('Instance of FetchPlease', () => { 11 | const PATH = '/api'; 12 | const TIMEOUT = 1000; 13 | const HEADERS = { 14 | Accept: 'application/json' 15 | }; 16 | 17 | let api = new FetchPlease(PATH, { 18 | timeout: TIMEOUT, 19 | headers: HEADERS 20 | }); 21 | 22 | it('has all required properties', () => { 23 | expect(api).to.be.an.instanceof(FetchPlease); 24 | expect(api.path).to.equal(PATH); 25 | expect(api.timeout).to.equal(TIMEOUT); 26 | expect(api.headers).to.deep.equal(HEADERS); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/get.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import {expect} from 'chai'; 3 | import FetchPlease from '../src/fetch-please'; 4 | 5 | let XMLHttpRequest = sinon.useFakeXMLHttpRequest(); 6 | 7 | describe('Method getRequest()', () => { 8 | before(function() { 9 | this.requests = []; 10 | this.api = new FetchPlease('/api/', {XMLHttpRequest}); 11 | 12 | XMLHttpRequest.onCreate = (xhr) => { 13 | this.requests.push(xhr); 14 | }; 15 | }); 16 | 17 | it('exists', function() { 18 | expect(this.api.getRequest).to.be.a('function'); 19 | }); 20 | 21 | it('sends request', function() { 22 | expect(this.requests.length).to.equal(0); 23 | 24 | let {xhr, promise} = this.api.getRequest('users'); 25 | 26 | expect(xhr).to.be.an.instanceof(XMLHttpRequest); 27 | expect(xhr.method).be.equal('GET'); 28 | expect(promise).to.be.an.instanceof(Promise); 29 | expect(this.requests.length).to.equal(1); 30 | 31 | xhr.respond(200, {'content-type': 'application/json'}, '{"a":1}'); 32 | 33 | return promise.then((data) => { 34 | expect(data).to.deep.equal({a: 1}); 35 | }); 36 | }); 37 | 38 | it('joins parameters', function() { 39 | let {xhr} = this.api.getRequest('users', {limit: 10, offset: 20, filter: 'Mary'}); 40 | expect(xhr.url).to.equal('/api/users?limit=10&offset=20&filter=Mary'); 41 | }); 42 | 43 | after(function() { 44 | this.api = null; 45 | this.requests = []; 46 | XMLHttpRequest.onCreate = null; 47 | }); 48 | }); 49 | 50 | describe('Method get()', () => { 51 | before(function() { 52 | this.requests = []; 53 | this.api = new FetchPlease('/api/', {XMLHttpRequest}); 54 | 55 | XMLHttpRequest.onCreate = (xhr) => { 56 | this.requests.push(xhr); 57 | }; 58 | }); 59 | 60 | it('exists', function() { 61 | expect(this.api.get).to.be.a('function'); 62 | }); 63 | 64 | it('sends request', function() { 65 | expect(this.requests.length).to.equal(0); 66 | 67 | let promise = this.api.get('users'); 68 | 69 | expect(this.requests.length).to.equal(1); 70 | 71 | let request = this.requests[0]; 72 | request.respond(200, {'Content-Type': 'application/json'}, '{"a":1}'); 73 | 74 | return promise.then((data) => { 75 | expect(data).to.deep.equal({a: 1}); 76 | }); 77 | }); 78 | 79 | after(function() { 80 | this.api = null; 81 | this.requests = []; 82 | XMLHttpRequest.onCreate = null; 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test/helpers.spec.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {assign, joinParams} from '../src/helpers'; 3 | 4 | describe('Helper assign()', () => { 5 | it('exist', () => { 6 | expect(assign).to.be.an.instanceof(Function); 7 | }); 8 | 9 | it('merged objects', () => { 10 | expect(assign({}, {a: 1})).to.deep.equal({a: 1}); 11 | expect(assign({a: 1}, {b: 2})).to.deep.equal({a: 1, b: 2}); 12 | }); 13 | 14 | it('merged objects in correct order', () => { 15 | expect(assign({a: 0}, {a: 1})).to.deep.equal({a: 1}); 16 | }); 17 | 18 | it('returns same target', () => { 19 | let target = {}, 20 | assigned = assign(target, {a: 1}); 21 | 22 | // expect((target === assigned)).to.be.ok; 23 | expect(target).to.equal(assigned); 24 | }); 25 | }); 26 | 27 | describe('Helper joinParams()', () => { 28 | it('exists', () => { 29 | expect(joinParams).to.be.an.instanceof(Function); 30 | }); 31 | 32 | it('handles missed arguments correctly', () => { 33 | expect(joinParams(null)).to.be.equal(null); 34 | expect(joinParams('')).to.be.equal(''); 35 | expect(joinParams('', null)).to.be.equal(''); 36 | expect(joinParams('', {})).to.be.equal(''); 37 | }); 38 | 39 | it('forms URL correctly', () => { 40 | expect(joinParams('', {a: 1})).to.be.equal('?a=1'); 41 | expect(joinParams('', {a: 1, b: 2})).to.be.equal('?a=1&b=2'); 42 | expect(joinParams('/?a=1', {b: 2})).to.be.equal('/?a=1&b=2'); 43 | expect(joinParams('/?', {a: 1})).to.be.equal('/?a=1'); 44 | 45 | expect(joinParams('/', {})).to.be.equal('/'); 46 | expect(joinParams('/', {a: 0})).to.be.equal('/?a=0'); 47 | expect(joinParams('/', {a: null})).to.be.equal('/'); 48 | expect(joinParams('/', {a: undefined})).to.be.equal('/'); 49 | 50 | expect(joinParams('/', {a: true})).to.be.equal('/?a=true'); 51 | expect(joinParams('/', {a: false})).to.be.equal('/?a=false'); 52 | }); 53 | 54 | it('decodes parameters correctly', () => { 55 | expect(joinParams('', {name: 'имя'})).to.be.equal('?name=%D0%B8%D0%BC%D1%8F'); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel-core/register 2 | --reporter dot 3 | --recursive 4 | --slow 20 5 | --growl 6 | -------------------------------------------------------------------------------- /test/post.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import {expect} from 'chai'; 3 | import FetchPlease from '../src/fetch-please'; 4 | 5 | let XMLHttpRequest = sinon.useFakeXMLHttpRequest(); 6 | 7 | describe('Method postRequest()', () => { 8 | before(function() { 9 | this.requests = []; 10 | this.api = new FetchPlease('/api/', {XMLHttpRequest}); 11 | 12 | XMLHttpRequest.onCreate = (xhr) => { 13 | this.requests.push(xhr); 14 | }; 15 | }); 16 | 17 | it('exists', function() { 18 | expect(this.api.postRequest).to.be.a('function'); 19 | }); 20 | 21 | it('sends request', function() { 22 | expect(this.requests.length).to.equal(0); 23 | 24 | let {xhr, promise} = this.api.postRequest('users', null); 25 | 26 | expect(xhr).to.be.an.instanceof(XMLHttpRequest); 27 | expect(xhr.method).be.equal('POST'); 28 | expect(promise).to.be.an.instanceof(Promise); 29 | expect(this.requests.length).to.equal(1); 30 | 31 | xhr.respond(200, {'content-type': 'application/json'}, '{"a":1}'); 32 | 33 | return promise.then((data) => { 34 | expect(data).to.deep.equal({a: 1}); 35 | }); 36 | }); 37 | 38 | it('sends request with string', function() { 39 | let {xhr} = this.api.postRequest('users', 'Hello'); 40 | expect(xhr.requestBody).be.equal('Hello'); 41 | }); 42 | 43 | it('sends request with JSON', function() { 44 | let {xhr} = this.api.postRequest('users', { 45 | name: 'Mary', 46 | sirname: 'Brown' 47 | }); 48 | 49 | expect(xhr.requestBody).be.equal('{"name":"Mary","sirname":"Brown"}'); 50 | }); 51 | 52 | after(function() { 53 | this.api = null; 54 | this.requests = []; 55 | XMLHttpRequest.onCreate = null; 56 | }); 57 | }); 58 | 59 | describe('Method post()', () => { 60 | before(function() { 61 | this.requests = []; 62 | this.api = new FetchPlease('/api/', {XMLHttpRequest}); 63 | 64 | XMLHttpRequest.onCreate = (xhr) => { 65 | this.requests.push(xhr); 66 | }; 67 | }); 68 | 69 | it('exists', function() { 70 | expect(this.api.post).to.be.a('function'); 71 | }); 72 | 73 | it('sends request', function() { 74 | expect(this.requests.length).to.equal(0); 75 | 76 | let promise = this.api.post('users', null); 77 | 78 | expect(promise).to.be.an.instanceof(Promise); 79 | expect(this.requests.length).to.equal(1); 80 | 81 | this.requests[0].respond(200, {'content-type': 'application/json'}, '{"a":1}'); 82 | 83 | return promise.then((data) => { 84 | expect(data).to.deep.equal({a: 1}); 85 | }); 86 | }); 87 | 88 | after(function() { 89 | this.api = null; 90 | this.requests = []; 91 | XMLHttpRequest.onCreate = null; 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /test/put.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import {expect} from 'chai'; 3 | import FetchPlease from '../src/fetch-please'; 4 | 5 | let XMLHttpRequest = sinon.useFakeXMLHttpRequest(); 6 | 7 | describe('Method putRequest()', () => { 8 | before(function() { 9 | this.requests = []; 10 | this.api = new FetchPlease('/api/', {XMLHttpRequest}); 11 | 12 | XMLHttpRequest.onCreate = (xhr) => { 13 | this.requests.push(xhr); 14 | }; 15 | }); 16 | 17 | it('exists', function() { 18 | expect(this.api.putRequest).to.be.a('function'); 19 | }); 20 | 21 | it('sends request', function() { 22 | expect(this.requests.length).to.equal(0); 23 | 24 | let {xhr, promise} = this.api.putRequest('users', null); 25 | 26 | expect(xhr).to.be.an.instanceof(XMLHttpRequest); 27 | expect(xhr.method).be.equal('PUT'); 28 | expect(promise).to.be.an.instanceof(Promise); 29 | expect(this.requests.length).to.equal(1); 30 | 31 | xhr.respond(200, {'content-type': 'application/json'}, '{"a":1}'); 32 | 33 | return promise.then((data) => { 34 | expect(data).to.deep.equal({a: 1}); 35 | }); 36 | }); 37 | 38 | after(function() { 39 | this.api = null; 40 | this.requests = []; 41 | XMLHttpRequest.onCreate = null; 42 | }); 43 | }); 44 | 45 | describe('Method put()', () => { 46 | before(function() { 47 | this.requests = []; 48 | this.api = new FetchPlease('/api/', {XMLHttpRequest}); 49 | 50 | XMLHttpRequest.onCreate = (xhr) => { 51 | this.requests.push(xhr); 52 | }; 53 | }); 54 | 55 | it('exists', function() { 56 | expect(this.api.put).to.be.a('function'); 57 | }); 58 | 59 | it('sends request', function() { 60 | expect(this.requests.length).to.equal(0); 61 | 62 | let promise = this.api.put('users', null); 63 | 64 | expect(promise).to.be.an.instanceof(Promise); 65 | expect(this.requests.length).to.equal(1); 66 | 67 | this.requests[0].respond(200, {'content-type': 'application/json'}, '{"a":1}'); 68 | 69 | return promise.then((data) => { 70 | expect(data).to.deep.equal({a: 1}); 71 | }); 72 | }); 73 | 74 | after(function() { 75 | this.api = null; 76 | this.requests = []; 77 | XMLHttpRequest.onCreate = null; 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /test/request.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import {expect} from 'chai'; 3 | import FetchPlease, { 4 | ERROR_XHR_NOT_FOUND, ERROR_UNKNOWN_HTTP_METHOD, ERROR_UNACCEPTABLE_HTTP_CODE, 5 | ERROR_RESOURCE_ABORTED, ERROR_JSON_PARSE 6 | } from '../src/fetch-please'; 7 | 8 | let XMLHttpRequest = sinon.useFakeXMLHttpRequest(); 9 | 10 | describe('Method request()', () => { 11 | before(function() { 12 | this.api = new FetchPlease('/api/', {XMLHttpRequest}); 13 | }); 14 | 15 | it('exists', function() { 16 | expect(this.api.request).to.be.a('function'); 17 | }); 18 | 19 | it('handles missed and invalid arguments correctly', function() { 20 | let broken = new FetchPlease('/api/'); 21 | 22 | expect(() => { 23 | broken.request(); 24 | }).to.throw(ERROR_XHR_NOT_FOUND); 25 | 26 | expect(() => { 27 | this.api.request(); 28 | }).to.throw(ERROR_UNKNOWN_HTTP_METHOD); 29 | 30 | expect(() => { 31 | this.api.request('FETCH'); 32 | }).to.throw(ERROR_UNKNOWN_HTTP_METHOD); 33 | }); 34 | 35 | it('should join paths', function() { 36 | let api = new FetchPlease('/api/', {XMLHttpRequest}); 37 | 38 | // URLs without normilizing 39 | expect(api.request('GET', '/users').xhr.url).to.equal('/api//users'); 40 | expect(api.request('GET', 'users').xhr.url).to.equal('/api/users'); 41 | }); 42 | 43 | it('should set headers', function() { 44 | let preset = new FetchPlease('/api/', { 45 | XMLHttpRequest, 46 | headers: { 47 | 'Content-Type': 'application/json', 48 | 'X-Custom-Header': 'custom', 49 | 'X-Undefined-Header': undefined, 50 | 'X-Falsy-Header': false, 51 | 'X-Null-Header': null 52 | } 53 | }); 54 | 55 | let {xhr} = preset.request('GET', '/'), 56 | {requestHeaders} = xhr; 57 | 58 | /* eslint no-unused-expressions:0 */ 59 | expect(requestHeaders['Content-Type']).to.equal('application/json'); 60 | expect(requestHeaders['X-Custom-Header']).to.equal('custom'); 61 | expect('X-Undefined-Header' in requestHeaders).to.be.false; 62 | expect('X-Falsy-Header' in requestHeaders).to.be.false; 63 | expect('X-Null-Header' in requestHeaders).to.be.false; 64 | }); 65 | 66 | it('should set headers from callback', function() { 67 | let preset = new FetchPlease('/api/', { 68 | XMLHttpRequest, 69 | headers: () => { 70 | return { 71 | 'Content-Type': 'application/json', 72 | 'X-Custom-Header': 'custom' 73 | }; 74 | } 75 | }); 76 | 77 | let {xhr} = preset.request('GET', '/'), 78 | {requestHeaders} = xhr; 79 | 80 | expect(requestHeaders['Content-Type']).to.equal('application/json'); 81 | expect(requestHeaders['X-Custom-Header']).to.equal('custom'); 82 | }); 83 | 84 | it('should set timeout', function() { 85 | let preset = new FetchPlease('/api/', { 86 | XMLHttpRequest, 87 | timeout: 1 88 | }); 89 | 90 | expect(preset.timeout).to.equal(1); 91 | 92 | let {xhr} = preset.request('GET', '/'); 93 | 94 | expect(xhr.timeout).to.equal(1); 95 | }); 96 | 97 | it('should handle response without JSON', function() { 98 | let {xhr, promise} = this.api.request('GET', '/'); 99 | 100 | xhr.respond(200, {'Content-Type': 'text/html'}, '

Hi!

'); 101 | 102 | return promise 103 | .then((xhr) => { 104 | expect(xhr.responseText).to.equal('

Hi!

'); 105 | }); 106 | }); 107 | 108 | it('should change number of opened requests', function() { 109 | expect(this.api.opened.length).to.equal(0); 110 | 111 | let first = this.api.request('GET', '/'); 112 | expect(this.api.opened.length).to.equal(1); 113 | 114 | let second = this.api.request('GET', '/'); 115 | expect(this.api.opened.length).to.equal(2); 116 | 117 | first.xhr.respond(200, {'content-type': 'application/json'}, '{}'); 118 | second.xhr.respond(200, {'content-type': 'application/json'}, '{}'); 119 | 120 | return first.promise 121 | .then(() => { 122 | return second.promise; 123 | }) 124 | .then(() => { 125 | expect(this.api.opened.length).to.equal(0); 126 | }); 127 | }); 128 | 129 | it('should handle unacceptable HTTP code', function() { 130 | let {xhr, promise} = this.api.request('GET', '/'); 131 | 132 | expect(this.api.opened.length).to.equal(1); 133 | xhr.respond(404, {'content-type': 'application/json'}, '{}'); 134 | 135 | return promise.catch((error) => { 136 | expect(this.api.opened.length).to.equal(0); 137 | expect(error).to.be.an.instanceOf(Error); 138 | expect(error.message).to.equal(ERROR_UNACCEPTABLE_HTTP_CODE); 139 | expect(error.statusCode).to.equal(404); 140 | }); 141 | }); 142 | 143 | it('should handle invalid JSON', function() { 144 | let {xhr, promise} = this.api.request('GET', '/'); 145 | xhr.respond(200, {'content-type': 'application/json'}, '{blah}'); 146 | 147 | return promise.catch((error) => { 148 | expect(error).to.be.an.instanceOf(Error); 149 | expect(error.message).to.equal(ERROR_JSON_PARSE); 150 | }); 151 | }); 152 | 153 | it('should handle abort', function() { 154 | let {xhr, promise} = this.api.request('GET', '/'); 155 | xhr.abort(); 156 | 157 | return promise.catch((error) => { 158 | expect(error).to.be.an.instanceOf(Error); 159 | expect(error.message).to.equal(ERROR_RESOURCE_ABORTED); 160 | }); 161 | }); 162 | 163 | it('should handle progress', function() { 164 | function onProgress() { 165 | // Do nothing 166 | } 167 | 168 | let {xhr} = this.api.request('GET', '/', null, {onProgress}); 169 | 170 | expect(xhr.eventListeners.progress[0]).to.equal(onProgress); 171 | }); 172 | 173 | it('should handle progress upload', function() { 174 | function onProgressUpload() { 175 | // Do nothing 176 | } 177 | 178 | let {xhr} = this.api.request('GET', '/', null, {onProgressUpload}); 179 | 180 | expect(xhr.upload.eventListeners.progress[0]).to.equal(onProgressUpload); 181 | }); 182 | 183 | after(function() { 184 | this.api = null; 185 | }); 186 | }); 187 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'), 4 | webpack = require('webpack'), 5 | project = require('./package.json'), 6 | banner = _.template( 7 | '<%= name %> v<%= version %>\n' + 8 | '<%= description %>\n' + 9 | '@author <%= author.name %>, <%= author.url %>' 10 | )(project); 11 | 12 | var config = { 13 | module: { 14 | preLoaders: [ 15 | { 16 | test: /\.js$/, 17 | loader: 'eslint', 18 | exclude: /node_modules/ 19 | } 20 | ], 21 | loaders: [ 22 | { 23 | test: /\.js$/, 24 | loader: 'babel', 25 | exclude: /node_modules/ 26 | } 27 | ] 28 | }, 29 | output: { 30 | library: 'FetchPlease', 31 | libraryTarget: 'umd' 32 | }, 33 | resolve: { 34 | extensions: ['', '.js'] 35 | }, 36 | plugins: [ 37 | new webpack.optimize.OccurenceOrderPlugin(), 38 | new webpack.BannerPlugin(banner) 39 | ] 40 | }; 41 | 42 | if (process.env.NODE_ENV === 'production') { 43 | config.plugins.push(new webpack.optimize.UglifyJsPlugin({ 44 | compressor: { 45 | screw_ie8: true, 46 | warnings: false 47 | } 48 | })); 49 | } 50 | 51 | module.exports = config; 52 | --------------------------------------------------------------------------------