├── .editorconfig ├── .gitignore ├── .prettierrc ├── .prettierrc.js ├── .travis.yml ├── README.md ├── lerna.json ├── package.json ├── packages ├── xhr-mock-examples │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── axios │ │ │ ├── index.html │ │ │ └── index.js │ │ ├── jquery │ │ │ ├── index.html │ │ │ └── index.js │ │ ├── native │ │ │ ├── index.html │ │ │ └── index.js │ │ ├── proxy │ │ │ ├── index.html │ │ │ └── index.js │ │ └── superagent │ │ │ ├── index.html │ │ │ └── index.js │ └── webpack.config.js ├── xhr-mock-tests │ ├── package.json │ ├── src │ │ ├── _.ts │ │ ├── axios.test.ts │ │ ├── jquery.test.ts │ │ ├── native.test.ts │ │ ├── rxjs.test.ts │ │ └── superagent.test.ts │ ├── testem.js │ ├── tsconfig.json │ └── webpack.config.js └── xhr-mock │ ├── CHANGELOG.md │ ├── LICENSE.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── rollup.config.js │ ├── src │ ├── MockError.ts │ ├── MockEvent.ts │ ├── MockEventTarget.test.ts │ ├── MockEventTarget.ts │ ├── MockHeaders.ts │ ├── MockProgressEvent.ts │ ├── MockRequest.test.ts │ ├── MockRequest.ts │ ├── MockResponse.test.ts │ ├── MockResponse.ts │ ├── MockURL.test.ts │ ├── MockURL.ts │ ├── MockXMLHttpRequest.test.ts │ ├── MockXMLHttpRequest.ts │ ├── MockXMLHttpRequestEventTarget.test.ts │ ├── MockXMLHttpRequestEventTarget.ts │ ├── MockXMLHttpRequestUpload.ts │ ├── XHRMock.test.ts │ ├── XHRMock.ts │ ├── createMockFunction.test.ts │ ├── createMockFunction.ts │ ├── createResponseFromObject.ts │ ├── formatError.test.ts │ ├── formatError.ts │ ├── handle.ts │ ├── index.ts │ ├── index.umd.ts │ ├── isPromiseLike.ts │ ├── proxy.browser.test.ts │ ├── proxy.browser.ts │ ├── proxy.test.ts │ ├── proxy.ts │ ├── types.ts │ └── utils │ │ ├── delay.test.ts │ │ ├── delay.ts │ │ ├── once.test.ts │ │ ├── once.ts │ │ ├── sequence.test.ts │ │ └── sequence.ts │ ├── test │ ├── acceptance.test.ts │ ├── howto.test.ts │ ├── integration.test.ts │ ├── usage.test.ts │ └── util │ │ ├── recordXHREvents.test.js │ │ └── recordXHREvents.ts │ ├── tsconfig.json │ ├── tsconfig.test.json │ └── typings │ ├── global.d.ts │ └── url.d.ts ├── tsconfig.base.json ├── tslint.base.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # OS files 3 | .DS_Store 4 | 5 | # package manager files 6 | node_modules/ 7 | *.log 8 | npm-debug.log.* 9 | lerna-debug.log 10 | package.lock.json 11 | **/yarn.lock 12 | 13 | # IDE files 14 | .idea 15 | .vscode 16 | 17 | # generated files 18 | lib 19 | dist 20 | build 21 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "jsxBracketSameLine": false, 4 | "tabWidth": 2, 5 | "trailingComma": "none", 6 | "singleQuote": true, 7 | "semi": true 8 | } 9 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | singleQuote: true 4 | }; 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "10" 5 | - "12" 6 | 7 | # https://docs.travis-ci.com/user/gui-and-headless-browsers/#using-xvfb-to-run-tests-that-require-a-gui 8 | dist: xenial 9 | 10 | services: 11 | - xvfb 12 | 13 | addons: 14 | chrome: stable 15 | 16 | script: 17 | - yarn 18 | - yarn run bootstrap 19 | - yarn run ci 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xhr-mock 2 | 3 | This repo is a mono-repo managed by `lernajs`. 4 | 5 | ## [📖 Documentation](./packages/xhr-mock) 6 | 7 | The documentation for the main NPM package can be found [here](./packages/xhr-mock). 8 | 9 | ## 🛠 Development 10 | 11 | Install the dependencies: 12 | 13 | ```bash 14 | yarn 15 | yarn run bootstrap 16 | ``` 17 | 18 | ### The NPM package 19 | 20 | Build and test the package: 21 | 22 | ```bash 23 | cd packages/xhr-mock 24 | yarn run build # transpile the sources 25 | yarn run test # run the unit tests 26 | ``` 27 | 28 | ### The integration tests 29 | 30 | Test the package against a few well known XHR libraries: 31 | 32 | ```bash 33 | # NOTE: you need to build the main package first 34 | cd packages/xhr-mock-tests 35 | yarn run test # run the integration tests 36 | ``` 37 | 38 | ## 🎁 Contributing 39 | 40 | Contributors are very welcome! Please raise an issue or PR on Github. 41 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.6.0", 3 | "packages": [ 4 | "packages/*" 5 | ], 6 | "version": "2.5.1", 7 | "npmClient": "yarn" 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xhr-mock-monorepo", 3 | "private": true, 4 | "dependencies": { 5 | "husky": "^3.0.0", 6 | "lerna": "^2.6.0", 7 | "lint-staged": "^4.0.4", 8 | "prettier": "^1.9.2", 9 | "tslint": "^5.8.0", 10 | "tslint-microsoft-contrib": "^5.0.1", 11 | "typescript": "2.7.2" 12 | }, 13 | "scripts": { 14 | "bootstrap": "lerna bootstrap --npm-client yarn", 15 | "publish": "yarn run ci && lerna publish", 16 | "ci": "lerna run ci" 17 | }, 18 | "lint-staged": { 19 | "*.{js,json,ts,md}": [ 20 | "prettier --write", 21 | "git add" 22 | ] 23 | }, 24 | "husky": { 25 | "hooks": { 26 | "pre-commit": "lint-staged" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/xhr-mock-examples/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /packages/xhr-mock-examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xhr-mock-examples", 3 | "private": true, 4 | "dependencies": { 5 | "axios": "^0.19.0", 6 | "jquery": "^3.2.1", 7 | "superagent": "^3.5.2", 8 | "webpack-dev-server": "^2.10.0", 9 | "xhr-mock": "file:.." 10 | }, 11 | "devDependencies": { 12 | "copy-webpack-plugin": "^4.0.1", 13 | "linklocal": "^2.8.0", 14 | "webpack": "^2.6.0" 15 | }, 16 | "scripts": { 17 | "clean": "rm -rf dist", 18 | "build": "webpack", 19 | "watch": "webpack-dev-server" 20 | }, 21 | "version": "2.5.1" 22 | } 23 | -------------------------------------------------------------------------------- /packages/xhr-mock-examples/src/axios/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Axios Example — xhr-mock 6 | 7 | 8 | 9 |

xhr-mock

10 |

Axios Example

11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/xhr-mock-examples/src/axios/index.js: -------------------------------------------------------------------------------- 1 | import mock from 'xhr-mock'; 2 | import axios from 'axios'; 3 | 4 | mock.setup(); 5 | 6 | mock.get('http://google.com/', { 7 | body: '

Google

' 8 | }); 9 | 10 | // --------- 11 | 12 | axios.get('http://google.com/').then( 13 | res => { 14 | console.log('loaded', res.data); 15 | }, 16 | error => { 17 | console.log('ERROR', error); 18 | } 19 | ); 20 | 21 | // --------- 22 | -------------------------------------------------------------------------------- /packages/xhr-mock-examples/src/jquery/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | jQuery Example — xhr-mock 6 | 7 | 8 | 9 |

xhr-mock

10 |

jQuery Example

11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/xhr-mock-examples/src/jquery/index.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import mock from 'xhr-mock'; 3 | 4 | mock.setup(); 5 | 6 | mock.get('http://google.com/', { 7 | body: '

Google

' 8 | }); 9 | 10 | // --------- 11 | 12 | $.get('http://google.com/', (data, textStatus, jqXHR) => { 13 | console.log('loaded', data); 14 | }).fail(() => { 15 | console.log('ERROR', arguments); 16 | }); 17 | 18 | // --------- 19 | -------------------------------------------------------------------------------- /packages/xhr-mock-examples/src/native/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Native Example — xhr-mock 6 | 7 | 8 | 9 |

xhr-mock

10 |

Native Example

11 | 12 | 13 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/xhr-mock-examples/src/native/index.js: -------------------------------------------------------------------------------- 1 | import mock from 'xhr-mock'; 2 | 3 | mock.setup(); 4 | 5 | mock.get('http://google.com/', { 6 | body: '

Google

' 7 | }); 8 | 9 | // --------- 10 | 11 | const xhr = new XMLHttpRequest(); 12 | 13 | xhr.open('GET', 'http://google.com/'); 14 | 15 | xhr.onload = function() { 16 | console.log('loaded', this.responseText); 17 | }; 18 | 19 | xhr.onerror = function() { 20 | console.log('error'); 21 | }; 22 | 23 | xhr.send(); 24 | 25 | // --------- 26 | -------------------------------------------------------------------------------- /packages/xhr-mock-examples/src/proxy/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Proxy Example — xhr-mock 6 | 7 | 8 | 9 |

xhr-mock

10 |

Proxy Example

11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/xhr-mock-examples/src/proxy/index.js: -------------------------------------------------------------------------------- 1 | import mock, {proxy} from 'xhr-mock'; 2 | 3 | mock.setup(); 4 | 5 | mock.get('http://localhost/api/speak', { 6 | body: JSON.stringify({message: 'Hello World!'}) 7 | }); 8 | 9 | mock.use(proxy); 10 | 11 | // --------- 12 | 13 | function fetch(url) { 14 | const xhr = new XMLHttpRequest(); 15 | 16 | xhr.open('GET', url); 17 | 18 | xhr.onload = function() { 19 | console.log(`loaded ${url}:`, this.responseText); 20 | }; 21 | 22 | xhr.onerror = function() { 23 | console.log(`error loading ${url}`); 24 | }; 25 | 26 | xhr.send(); 27 | } 28 | 29 | fetch('http://localhost/api/speak'); 30 | fetch('https://jsonplaceholder.typicode.com/users/1'); 31 | 32 | // --------- 33 | -------------------------------------------------------------------------------- /packages/xhr-mock-examples/src/superagent/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Superagent Example — xhr-mock 6 | 7 | 8 | 9 |

xhr-mock

10 |

Superagent Example

11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/xhr-mock-examples/src/superagent/index.js: -------------------------------------------------------------------------------- 1 | import mock from 'xhr-mock'; 2 | import superagent from 'superagent'; 3 | 4 | mock.setup(); 5 | 6 | mock.get('http://google.com/', { 7 | body: '

Google

' 8 | }); 9 | 10 | // --------- 11 | 12 | superagent.get('http://google.com/', (err, res) => { 13 | if (err) return console.log('ERROR', arguments); 14 | console.log('loaded', res.text); 15 | }); 16 | 17 | // --------- 18 | -------------------------------------------------------------------------------- /packages/xhr-mock-examples/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 3 | 4 | module.exports = { 5 | 6 | context: path.resolve('src'), 7 | 8 | entry: { 9 | native: './native/index.js', 10 | axios: './axios/index.js', 11 | jquery: './native/index.js', 12 | superagent: './superagent/index.js', 13 | proxy: './proxy/index.js' 14 | }, 15 | 16 | output: { 17 | path: path.resolve('dist'), 18 | filename: '[name]/index.js' 19 | }, 20 | 21 | plugins: [new CopyWebpackPlugin([{from: '**/*.html'}])] 22 | 23 | }; 24 | -------------------------------------------------------------------------------- /packages/xhr-mock-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xhr-mock-tests", 3 | "private": true, 4 | "dependencies": { 5 | "@types/url-search-params": "^0.10.1", 6 | "axios": "^0.19.0", 7 | "core-js": "^2.5.3", 8 | "jquery": "^3.2.1", 9 | "rxjs": "^5.5.6", 10 | "superagent": "^3.8.2", 11 | "url-search-params": "^0.10.0", 12 | "xhr-mock": "^2.5.1" 13 | }, 14 | "devDependencies": { 15 | "@types/chai": "4.1.0", 16 | "@types/jquery": "^3.2.17", 17 | "@types/mocha": "^2.2.46", 18 | "@types/superagent": "^3.5.6", 19 | "chai": "^4.1.2", 20 | "concurrently": "^3.5.1", 21 | "glob": "^7.1.2", 22 | "testem": "^1.18.4", 23 | "ts-loader": "^3.2.0", 24 | "tslint": "^5.9.1", 25 | "typescript": "2.7.2", 26 | "webpack": "^3.10.0" 27 | }, 28 | "scripts": { 29 | "clean": "rm -rf ./dist", 30 | "lint": "tslint -c ../../tslint.base.json 'src/**/*.ts'", 31 | "check": "tsc --pretty --noEmit --project tsconfig.json", 32 | "test": "webpack && testem ci", 33 | "test:watch": "concurrently 'webpack --watch' testem", 34 | "ci": "yarn run clean && yarn run lint && yarn run check && yarn run test" 35 | }, 36 | "version": "2.5.1" 37 | } 38 | -------------------------------------------------------------------------------- /packages/xhr-mock-tests/src/_.ts: -------------------------------------------------------------------------------- 1 | import 'core-js'; 2 | import URLSearchParams = require('url-search-params'); 3 | 4 | if (!window.URLSearchParams) { 5 | window.URLSearchParams = URLSearchParams; 6 | } 7 | -------------------------------------------------------------------------------- /packages/xhr-mock-tests/src/axios.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import axios from 'axios'; 3 | import mock from 'xhr-mock'; 4 | 5 | describe('axios', () => { 6 | beforeEach(() => mock.setup()); 7 | afterEach(() => mock.teardown()); 8 | 9 | it('should GET', async () => { 10 | mock.use((req, res) => { 11 | expect(req.method()).to.eq('GET'); 12 | expect(String(req.url())).to.eq('/'); 13 | expect(req.body()).to.eq(null); 14 | return res 15 | .status(200) 16 | .reason('OK') 17 | .header('Content-Length', '12') 18 | .body('Hello World!'); 19 | }); 20 | 21 | const res = await axios.get('/'); 22 | 23 | expect(res.status).to.eq(200); 24 | expect(res.statusText).to.eq('OK'); 25 | expect(res.headers).to.deep.eq({ 26 | 'content-length': '12' 27 | }); 28 | expect(res.data).to.eq('Hello World!'); 29 | }); 30 | 31 | it('should POST', async () => { 32 | mock.use((req, res) => { 33 | expect(req.method()).to.eq('POST'); 34 | expect(String(req.url())).to.eq('/'); 35 | expect(req.body()).to.eq(JSON.stringify({foo: 'bar'})); 36 | return res 37 | .status(201) 38 | .reason('Created') 39 | .header('Content-Length', '12') 40 | .body('Hello World!'); 41 | }); 42 | 43 | const res = await axios.post('/', {foo: 'bar'}); 44 | 45 | expect(res.status).to.eq(201); 46 | expect(res.statusText).to.eq('Created'); 47 | expect(res.headers).to.deep.eq({ 48 | 'content-length': '12' 49 | }); 50 | expect(res.data).to.eq('Hello World!'); 51 | }); 52 | 53 | it('should PUT', async () => { 54 | mock.use((req, res) => { 55 | expect(req.method()).to.eq('PUT'); 56 | expect(String(req.url())).to.eq('/'); 57 | expect(req.body()).to.eq(JSON.stringify({foo: 'bar'})); 58 | return res 59 | .status(200) 60 | .reason('Created') 61 | .header('Content-Length', '12') 62 | .body('Hello World!'); 63 | }); 64 | 65 | const res = await axios.put('/', {foo: 'bar'}); 66 | 67 | expect(res.status).to.eq(200); 68 | expect(res.statusText).to.eq('Created'); 69 | expect(res.headers).to.deep.eq({ 70 | 'content-length': '12' 71 | }); 72 | expect(res.data).to.eq('Hello World!'); 73 | }); 74 | 75 | it('should DELETE', async () => { 76 | mock.use((req, res) => { 77 | expect(req.method()).to.eq('DELETE'); 78 | expect(String(req.url())).to.eq('/'); 79 | expect(req.body()).to.eq(null); 80 | return res.status(204).reason('No Content'); 81 | }); 82 | 83 | const res = await axios.delete('/'); 84 | 85 | expect(res.status).to.eq(204); 86 | expect(res.statusText).to.eq('No Content'); 87 | expect(res.headers).to.deep.eq({}); 88 | expect(res.data).to.eq(''); 89 | }); 90 | 91 | it('should time out', async () => { 92 | mock.get('/', () => new Promise(() => {})); 93 | 94 | try { 95 | const res = await axios.get('/', {timeout: 10}); 96 | expect.fail(); 97 | } catch (error) { 98 | expect(error).to.be.an('Error'); 99 | expect(error.message.toLowerCase()).to.contain('timeout'); 100 | } 101 | }); 102 | 103 | it('should error', async () => { 104 | mock.get('/', () => Promise.reject(new Error('😬'))); 105 | 106 | try { 107 | const res = await axios.get('/'); 108 | expect.fail(); 109 | } catch (error) { 110 | expect(error).to.be.an('Error'); 111 | expect(error.message).to.contain('Network Error'); 112 | } 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /packages/xhr-mock-tests/src/jquery.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import * as $ from 'jquery'; 3 | import mock from 'xhr-mock'; 4 | 5 | describe('jquery', () => { 6 | beforeEach(() => mock.setup()); 7 | afterEach(() => mock.teardown()); 8 | 9 | it('should GET', async () => { 10 | mock.use((req, res) => { 11 | expect(req.method()).to.eq('GET'); 12 | expect(String(req.url())).to.eq('/'); 13 | expect(req.body()).to.eq(null); 14 | return res 15 | .status(200) 16 | .reason('OK') 17 | .header('Content-Length', '12') 18 | .body('Hello World!'); 19 | }); 20 | 21 | await $.ajax('/') 22 | .then((data, status, xhr) => { 23 | expect(xhr.status).to.eq(200); 24 | expect(xhr.statusText).to.eq('OK'); 25 | expect(xhr.getAllResponseHeaders()).to.contain( 26 | 'content-length: 12\r\n' 27 | ); 28 | expect(data).to.eq('Hello World!'); 29 | }) 30 | .catch((xhr, status, error) => expect.fail(error)); 31 | }); 32 | 33 | it('should POST', async () => { 34 | mock.use((req, res) => { 35 | expect(req.method()).to.eq('POST'); 36 | expect(String(req.url())).to.eq('/'); 37 | expect(req.body()).to.eq(JSON.stringify({foo: 'bar'})); 38 | return res 39 | .status(201) 40 | .reason('Created') 41 | .header('Content-Length', '12') 42 | .body('Hello World!'); 43 | }); 44 | 45 | const res = await $.ajax({ 46 | method: 'post', 47 | url: '/', 48 | data: JSON.stringify({foo: 'bar'}) 49 | }) 50 | .then((data, status, xhr) => { 51 | expect(xhr.status).to.eq(201); 52 | expect(xhr.statusText).to.eq('Created'); 53 | expect(xhr.getAllResponseHeaders()).to.contain( 54 | 'content-length: 12\r\n' 55 | ); 56 | expect(data).to.eq('Hello World!'); 57 | }) 58 | .catch((xhr, status, error) => expect.fail(error)); 59 | }); 60 | 61 | it('should PUT', async () => { 62 | mock.use((req, res) => { 63 | expect(req.method()).to.eq('PUT'); 64 | expect(String(req.url())).to.eq('/'); 65 | expect(req.body()).to.eq(JSON.stringify({foo: 'bar'})); 66 | return res 67 | .status(200) 68 | .reason('OK') 69 | .header('Content-Length', '12') 70 | .body('Hello World!'); 71 | }); 72 | 73 | const res = await $.ajax({ 74 | method: 'put', 75 | url: '/', 76 | data: JSON.stringify({foo: 'bar'}) 77 | }) 78 | .then((data, status, xhr) => { 79 | expect(xhr.status).to.eq(200); 80 | expect(xhr.statusText).to.eq('OK'); 81 | expect(xhr.getAllResponseHeaders()).to.contain( 82 | 'content-length: 12\r\n' 83 | ); 84 | expect(data).to.eq('Hello World!'); 85 | }) 86 | .catch((xhr, status, error) => expect.fail(error)); 87 | }); 88 | 89 | it('should DELETE', async () => { 90 | mock.use((req, res) => { 91 | expect(req.method()).to.eq('DELETE'); 92 | expect(String(req.url())).to.eq('/'); 93 | expect(req.body()).to.eq(null); 94 | return res.status(204).reason('No Content'); 95 | }); 96 | 97 | const res = await $.ajax({ 98 | method: 'delete', 99 | url: '/' 100 | }) 101 | .then((data, status, xhr) => { 102 | expect(xhr.status).to.eq(204); 103 | expect(xhr.statusText).to.eq('No Content'); 104 | expect(xhr.getAllResponseHeaders()).to.eq(''); 105 | expect(data).to.eq(undefined); 106 | }) 107 | .catch((xhr, status, error) => expect.fail(error)); 108 | }); 109 | 110 | it('should time out', async () => { 111 | mock.get('/', () => new Promise(() => {})); 112 | 113 | await $.ajax({ 114 | url: '/', 115 | timeout: 10 116 | }).then( 117 | () => expect.fail(), 118 | (xhr, status, error) => { 119 | expect(status).to.eq('timeout'); 120 | expect(error).to.contain(''); 121 | } 122 | ); 123 | }); 124 | 125 | it('should error', async () => { 126 | mock.get('/', () => Promise.reject(new Error('😬'))); 127 | 128 | await $.ajax('/').then( 129 | () => expect.fail(), 130 | (xhr, status, error) => { 131 | expect(status).to.eq('error'); 132 | expect(error).to.contain(''); 133 | } 134 | ); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /packages/xhr-mock-tests/src/native.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import mock from 'xhr-mock'; 3 | 4 | describe('native', () => { 5 | beforeEach(() => mock.setup()); 6 | afterEach(() => mock.teardown()); 7 | 8 | it('should receive a request containing a blob', done => { 9 | mock.post('/files', (req, res) => { 10 | expect(req.header('content-type')).to.equal('image/png'); 11 | expect(req.body()).to.equal(data); 12 | return res; 13 | }); 14 | 15 | const data = new Blob(['

Hello World!

'], {type: 'image/png'}); 16 | 17 | const req = new XMLHttpRequest(); 18 | req.open('POST', '/files'); 19 | req.onload = () => { 20 | done(); 21 | }; 22 | req.send(data); 23 | }); 24 | 25 | it('should receive a request containing form data', done => { 26 | mock.post('/contact', (req, res) => { 27 | expect(req.header('content-type')).to.equal( 28 | 'multipart/form-data; boundary=----XHRMockFormBoundary' 29 | ); 30 | expect(req.body()).to.equal(data); 31 | return res; 32 | }); 33 | 34 | const data = new FormData(); 35 | data.append('name', 'John Smith'); 36 | data.append('email', 'john@smith.com'); 37 | data.append('message', 'blah\nblah\nblah'); 38 | 39 | const req = new XMLHttpRequest(); 40 | req.open('POST', '/contact'); 41 | req.onload = () => { 42 | done(); 43 | }; 44 | req.send(data); 45 | }); 46 | 47 | it('should receive a request containing url data', done => { 48 | mock.post('/contact', (req, res) => { 49 | expect(req.header('content-type')).to.equal( 50 | 'application/x-www-form-urlencoded; charset=UTF-8' 51 | ); 52 | expect(req.body()).to.equal(data); 53 | return res; 54 | }); 55 | 56 | const data = new URLSearchParams(); 57 | data.append('name', 'John Smith'); 58 | data.append('email', 'john@smith.com'); 59 | data.append('message', 'blah\nblah\nblah'); 60 | 61 | const req = new XMLHttpRequest(); 62 | req.open('POST', '/contact'); 63 | req.onload = () => { 64 | done(); 65 | }; 66 | req.send(data); 67 | }); 68 | 69 | it('should receive a request containing string data', done => { 70 | mock.post('/echo', (req, res) => { 71 | expect(req.header('content-type')).to.equal('text/plain; charset=UTF-8'); 72 | expect(req.body()).to.equal('Hello World!'); 73 | return res; 74 | }); 75 | 76 | const data = 'Hello World!'; 77 | 78 | const req = new XMLHttpRequest(); 79 | req.open('POST', '/echo'); 80 | req.onload = () => { 81 | done(); 82 | }; 83 | req.send(data); 84 | }); 85 | 86 | it('should send a response containing an array buffer', done => { 87 | mock.get('/myfile.png', { 88 | body: new ArrayBuffer(0) 89 | }); 90 | 91 | // sourced from https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Sending_and_Receiving_Binary_Data 92 | const req = new XMLHttpRequest(); 93 | req.open('GET', '/myfile.png'); 94 | req.responseType = 'arraybuffer'; 95 | req.onload = () => { 96 | const arrayBuffer = req.response; 97 | expect(arrayBuffer).to.be.an('ArrayBuffer'); 98 | done(); 99 | }; 100 | req.send(null); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /packages/xhr-mock-tests/src/rxjs.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {ajax} from 'rxjs/observable/dom/ajax'; 3 | import {map, retryWhen, scan, delay} from 'rxjs/operators'; 4 | import mock from 'xhr-mock'; 5 | 6 | describe('rxjs', () => { 7 | beforeEach(() => mock.setup()); 8 | afterEach(() => mock.teardown()); 9 | 10 | it('should return a JSON object', done => { 11 | mock.post('/some-url', { 12 | body: JSON.stringify({data: 'mockdata'}) 13 | }); 14 | 15 | ajax({ 16 | url: '/some-url', 17 | body: {some: 'something'}, 18 | method: 'POST', 19 | headers: { 20 | 'Content-Type': 'application/json' 21 | }, 22 | responseType: 'json' 23 | }).subscribe({ 24 | next: response => { 25 | try { 26 | expect(response.response).to.be.deep.equal({ 27 | data: 'mockdata' 28 | }); 29 | } catch (error) { 30 | done(error); 31 | } 32 | }, 33 | error: error => done(error), 34 | complete: () => done() 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /packages/xhr-mock-tests/src/superagent.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import * as superagent from 'superagent'; 3 | import mock from 'xhr-mock'; 4 | 5 | describe('superagent', () => { 6 | beforeEach(() => mock.setup()); 7 | afterEach(() => mock.teardown()); 8 | 9 | it('should GET', async () => { 10 | mock.use((req, res) => { 11 | expect(req.method()).to.eq('GET'); 12 | expect(String(req.url())).to.eq('/'); 13 | expect(req.body()).to.eq(null); 14 | return res 15 | .status(200) 16 | .reason('OK') 17 | .header('Content-Length', '12') 18 | .body('Hello World!'); 19 | }); 20 | 21 | const res = await superagent.get('/'); 22 | 23 | expect(res.status).to.eq(200); 24 | // expect(res.statusText).to.eq('OK'); 25 | expect(res.header).to.have.property('content-length', '12'); 26 | expect(res.text).to.eq('Hello World!'); 27 | }); 28 | 29 | it('should POST', async () => { 30 | mock.use((req, res) => { 31 | expect(req.method()).to.eq('POST'); 32 | expect(String(req.url())).to.eq('/'); 33 | expect(req.body()).to.eq(JSON.stringify({foo: 'bar'})); 34 | return res 35 | .status(201) 36 | .reason('Created') 37 | .header('Content-Length', '12') 38 | .body('Hello World!'); 39 | }); 40 | 41 | const res = await superagent.post('/').send({foo: 'bar'}); 42 | 43 | expect(res.status).to.eq(201); 44 | // expect(res.statusText).to.eq('Created'); 45 | expect(res.header).to.have.property('content-length', '12'); 46 | expect(res.text).to.eq('Hello World!'); 47 | }); 48 | 49 | it('should PUT', async () => { 50 | mock.use((req, res) => { 51 | expect(req.method()).to.eq('PUT'); 52 | expect(String(req.url())).to.eq('/'); 53 | expect(req.body()).to.eq(JSON.stringify({foo: 'bar'})); 54 | return res 55 | .status(200) 56 | .reason('Created') 57 | .header('Content-Length', '12') 58 | .body('Hello World!'); 59 | }); 60 | 61 | const res = await superagent.put('/').send({foo: 'bar'}); 62 | 63 | expect(res.status).to.eq(200); 64 | // expect(res.statusText).to.eq('Created'); 65 | expect(res.header).to.have.property('content-length', '12'); 66 | expect(res.text).to.eq('Hello World!'); 67 | }); 68 | 69 | it('should DELETE', async () => { 70 | mock.use((req, res) => { 71 | expect(req.method()).to.eq('DELETE'); 72 | expect(String(req.url())).to.eq('/'); 73 | expect(req.body()).to.eq(null); 74 | return res.status(204).reason('No Content'); 75 | }); 76 | 77 | const res = await superagent.delete('/'); 78 | 79 | expect(res.status).to.eq(204); 80 | // expect(res.statusText).to.eq('No Content'); 81 | expect(res.header).not.to.have.property('content-length', '12'); 82 | expect(res.text).to.eq(''); 83 | }); 84 | 85 | it('should time out', async () => { 86 | mock.get('/', () => new Promise(() => {})); 87 | 88 | try { 89 | const res = await superagent.get('/').timeout({ 90 | response: 5, 91 | deadline: 6 92 | }); 93 | expect.fail(); 94 | } catch (error) { 95 | expect(error).to.be.an('Error'); 96 | expect(error.message.toLowerCase()).to.contain('timeout'); 97 | } 98 | }); 99 | 100 | it('should error', async () => { 101 | mock.get('/', () => Promise.reject(new Error('😬'))); 102 | 103 | try { 104 | const res = await superagent.get('/'); 105 | expect.fail(); 106 | } catch (error) { 107 | expect(error).to.be.an('Error'); 108 | expect(error.message.toLowerCase()).to.contain('terminated'); 109 | } 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /packages/xhr-mock-tests/testem.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | framework: 'mocha', 3 | reporter: 'dot', 4 | src_files: [ 5 | ], 6 | serve_files: [ 7 | './dist/tests.js' 8 | ], 9 | browser_args: { 10 | Chromium: [ 11 | '--no-sandbox' 12 | ], 13 | Chrome: [ 14 | '--no-sandbox' 15 | ] 16 | } 17 | } -------------------------------------------------------------------------------- /packages/xhr-mock-tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": ["src/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/xhr-mock-tests/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const glob = require('glob'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | context: path.resolve('.'), 7 | entry: ['./src/_.ts', ...glob.sync('./src/**/*.test.ts')], 8 | output: { 9 | path: path.resolve('dist'), 10 | filename: 'tests.js' 11 | }, 12 | resolve: { 13 | extensions: ['.ts', '.tsx', '.js'] 14 | }, 15 | module: { 16 | rules: [ 17 | { test: /\.tsx?$/, loader: 'ts-loader' } 18 | ] 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /packages/xhr-mock/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | # 2.5.1 4 | 5 | - fix: resolve import and local declaration name conflict to support Typescript@3.7 ([#92](https://github.com/jameslnewell/xhr-mock/pull/92)) ([more info](https://devblogs.microsoft.com/typescript/announcing-typescript-3-7/#local-and-imported-type-declarations-now-conflict)) 6 | 7 | # 2.5.0 8 | 9 | - added the `sequence()` utility function ([#83](https://github.com/jameslnewell/xhr-mock/pull/83)) 10 | 11 | # 2.4.1 12 | 13 | - fix: check for DOM specific classes before checking for an instance of them so that environments without them e.g. `mocha` without `jsdom` mostly works 14 | 15 | # 2.4.0 16 | 17 | - added `once` and `delay` utility functions. 18 | - changed the signature of `MockURL.query` from `{}` to `{[name: string]: string}` 19 | 20 | # 2.3.2 21 | 22 | - fix: proxy all response bodies, not just text ([#62](https://github.com/jameslnewell/xhr-mock/issues/62)) 23 | 24 | # 2.3.1 25 | 26 | - fix: format the default error message better ([#57](https://github.com/jameslnewell/xhr-mock/issues/57#issuecomment-376489889)) 27 | - fix: IE11 which chokes on `URLSearchParams` ([#58](https://github.com/jameslnewell/xhr-mock/pull/58)) 28 | 29 | # 2.3.0 30 | 31 | - added support for requests with `Blob`, `FormData` or `URLSearchParams` bodies 32 | - log errors thrown/rejected in handlers by default but allow logging to be customised 33 | 34 | # 2.2.0 35 | 36 | - added "support" for `responseType` of `arraybuffer`, `blob` and `document` by returning whatever object `res.body(body)` is set to 37 | 38 | ## 2.1.0 39 | 40 | - added support for `responseType="json"` 41 | 42 | ## 2.0.4 43 | 44 | - fix: improve compliance of `.response` 45 | 46 | ## 2.0.3 47 | 48 | - improving the documentation 49 | 50 | ## 2.0.2 51 | 52 | - fix: version badge on `README.md` 53 | 54 | ## 2.0.1 55 | 56 | - fix: undefined `__generator` in UMD bundle due to [#85](https://github.com/rollup/rollup-plugin-typescript/issues/85) 57 | 58 | ## 2.0.0 59 | 60 | - released with updated docs 61 | 62 | ## 2.0.0-preivew.15 63 | 64 | - fix (potential break): when `async=false` `loadstart` and `progress` events are no longer emitted according to the spec 65 | - fix (potential break): on `error`, `timeout` and `abort` the correct sequence of events are fired ([#41](https://github.com/jameslnewell/xhr-mock/issues/41)) 66 | - fix (potential break): changed the `error` and `onabort` to be `ProgressEvent`s like the latest spec ([and different to the typescript types](https://github.com/Microsoft/TypeScript/issues/19830)) 67 | 68 | ## 2.0.0-preivew.14 69 | 70 | - fix: made the proxy work in NodeJS 71 | 72 | ## 2.0.0-preivew.13 73 | 74 | - fix: examples in `README.md` 75 | 76 | ## 2.0.0-preivew.12 77 | 78 | - added a non-minified UMD bundle - `./dist/xhr-mock.js` 79 | 80 | ## 2.0.0-preivew.11 81 | 82 | - added `proxy` - a handler for proxying requests as real XHR 83 | - added `XHRMock.RealXMLHttpRequest` 84 | - deprecated `XHRMock.mock()` in favor of `XHRMock.use()` 85 | - removed `debugger` statements and added linting 86 | - fix: made `MockXMLHttpRequest` implement `XMLHttpRequest` and missing enum values on the instance e.g. `DONE` 87 | 88 | ## 2.0.0-preview.10 89 | 90 | - fixed a bug where the `body` would not be sent when it was an empty string ([#32](https://github.com/jameslnewell/xhr-mock/issues/32)) 91 | 92 | ## 2.0.0-preview.9 93 | 94 | - added support for `RegExp` in typings ([#36](https://github.com/jameslnewell/xhr-mock/pull/36)) 95 | 96 | ## 2.0.0-preview.8 97 | 98 | - added `typings` to `package.json` 99 | 100 | ## 2.0.0-preview.6 101 | 102 | - break: removed `MockRequest.progress()` and `MockResponse.progress()` 103 | 104 | ## 2.0.0-preview.5 105 | 106 | - added an export for the real `XMLHttpRequest` object 107 | - fix: made `MockObject` props optional 108 | - break: changed the signature of the `URL` returned from `MockRequest.url()` 109 | 110 | ## 2.0.0-preview.4 111 | 112 | - fix: fixed a bug with upload progress 113 | 114 | ## 2.0.0-preview.3 115 | 116 | - fix: include transpiled files in published package 117 | 118 | ## 2.0.0-preview.2 119 | 120 | - added types 121 | - added support for mock objects 122 | - break: changed the ordering of `MockRequest.progress()` and `MockRequest.progress()` 123 | 124 | ## 2.0.0-preview.1 125 | 126 | - added support for upload progress 127 | - break: renamed `MockResponse.statusText()` to `MockResponse.reason()` 128 | - break: removed `MockRequest.query()` and changed `MockRequest.url()` to return a URL object (with a `.toString()` method) 129 | - break: removed `MockResponse.timeout()` - instead, return a promise that never resolves 130 | - break: moved `MockRequest.progress()` to `MockResponse.progress()` and added `MockRequest.progress()` 131 | - break: removed support for [`component`](https://github.com/componentjs/component) 132 | 133 | ## 1.9.1 134 | 135 | - fixed [#30](https://github.com/jameslnewell/xhr-mock/issues/30) 136 | 137 | ## 1.9.0 138 | 139 | - added `Response.statusText()` for setting the status text 140 | 141 | ## 1.8.0 142 | 143 | - added support for regexes instead of URLs in all the mock methods 144 | - added the `.query()` method to the request object 145 | - added the `.reset()` method to `mock` and `MockXMLHttpRequest` 146 | - added `withCredentials` to the mocked XHR objects (used by some libraries to test for "real" XHR support) 147 | 148 | ## 1.7.0 149 | 150 | - added support for `addEventListener` ([#15](https://github.com/jameslnewell/xhr-mock/pull/15)) 151 | -------------------------------------------------------------------------------- /packages/xhr-mock/LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 James Newell 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /packages/xhr-mock/README.md: -------------------------------------------------------------------------------- 1 | # xhr-mock 2 | 3 | [![npm (tag)](https://img.shields.io/npm/v/xhr-mock.svg)]() 4 | [![Build Status](https://travis-ci.org/jameslnewell/xhr-mock.svg?branch=master)](https://travis-ci.org/jameslnewell/xhr-mock) 5 | [![npm](https://img.shields.io/npm/dm/localeval.svg)]() 6 | 7 | Utility for mocking `XMLHttpRequest`. 8 | 9 | Great for testing. Great for prototyping while your backend is still being built. 10 | 11 | Works in NodeJS and in the browser. Is compatible with [Axios](https://www.npmjs.com/package/axios), [jQuery](https://www.npmjs.com/package/jquery), [Superagent](https://www.npmjs.com/package/superagent) 12 | and probably every other library built on `XMLHttpRequest`. Standard compliant ([http://xhr.spec.whatwg.org/](http://xhr.spec.whatwg.org/)). 13 | 14 | ###### Documentation 15 | 16 | * [Installation](#installation) 17 | * [Usage](#usage) 18 | * [API](#api) 19 | * [How to?](#how-to) 20 | 21 | ## Installation 22 | 23 | ### Using a bundler 24 | 25 | If you are using a bundler like [Webpack](https://www.npmjs.com/package/webpack) or [Browserify](https://www.npmjs.com/package/browserify) then install `xhr-mock` using `yarn` or `npm`: 26 | 27 | ```bash 28 | yarn add --dev xhr-mock 29 | ``` 30 | 31 | Now import `xhr-mock` and start using it in your scripts: 32 | 33 | ```js 34 | import mock from 'xhr-mock'; 35 | ``` 36 | 37 | ### Without a bundler 38 | 39 | If you aren't using a bundler like [Webpack](https://www.npmjs.com/package/webpack) or [Browserify](https://www.npmjs.com/package/browserify) then add this script to your HTML: 40 | 41 | ```html 42 | 43 | ``` 44 | 45 | Now you can start using the global, `XHRMock`, in your scripts. 46 | 47 | ## Usage 48 | 49 | First off lets write some code that uses `XMLHttpRequest`... 50 | 51 | `./createUser.js` 52 | 53 | ```js 54 | // we could have just as easily use Axios, jQuery, Superagent 55 | // or another package here instead of using the native XMLHttpRequest object 56 | 57 | export default function(data) { 58 | return new Promise((resolve, reject) => { 59 | const xhr = new XMLHttpRequest(); 60 | xhr.onreadystatechange = () => { 61 | if (xhr.readyState == XMLHttpRequest.DONE) { 62 | if (xhr.status === 201) { 63 | try { 64 | resolve(JSON.parse(xhr.responseText).data); 65 | } catch (error) { 66 | reject(error); 67 | } 68 | } else if (xhr.status) { 69 | try { 70 | reject(JSON.parse(xhr.responseText).error); 71 | } catch (error) { 72 | reject(error); 73 | } 74 | } else { 75 | reject(new Error('An error ocurred whilst sending the request.')); 76 | } 77 | } 78 | }; 79 | xhr.open('post', '/api/user'); 80 | xhr.setRequestHeader('Content-Type', 'application/json'); 81 | xhr.send(JSON.stringify({data: data})); 82 | }); 83 | } 84 | ``` 85 | 86 | Now lets test the code we've written... 87 | 88 | `./createUser.test.js` 89 | 90 | ```js 91 | import mock from 'xhr-mock'; 92 | import createUser from './createUser'; 93 | 94 | describe('createUser()', () => { 95 | // replace the real XHR object with the mock XHR object before each test 96 | beforeEach(() => mock.setup()); 97 | 98 | // put the real XHR object back and clear the mocks after each test 99 | afterEach(() => mock.teardown()); 100 | 101 | it('should send the data as JSON', async () => { 102 | expect.assertions(2); 103 | 104 | mock.post('/api/user', (req, res) => { 105 | expect(req.header('Content-Type')).toEqual('application/json'); 106 | expect(req.body()).toEqual('{"data":{"name":"John"}}'); 107 | return res.status(201).body('{"data":{"id":"abc-123"}}'); 108 | }); 109 | 110 | await createUser({name: 'John'}); 111 | }); 112 | 113 | it('should resolve with some data when status=201', async () => { 114 | expect.assertions(1); 115 | 116 | mock.post('/api/user', { 117 | status: 201, 118 | reason: 'Created', 119 | body: '{"data":{"id":"abc-123"}}' 120 | }); 121 | 122 | const user = await createUser({name: 'John'}); 123 | 124 | expect(user).toEqual({id: 'abc-123'}); 125 | }); 126 | 127 | it('should reject with an error when status=400', async () => { 128 | expect.assertions(1); 129 | 130 | mock.post('/api/user', { 131 | status: 400, 132 | reason: 'Bad request', 133 | body: '{"error":"A user named \\"John\\" already exists."}' 134 | }); 135 | 136 | try { 137 | const user = await createUser({name: 'John'}); 138 | } catch (error) { 139 | expect(error).toMatch('A user named "John" already exists.'); 140 | } 141 | }); 142 | }); 143 | ``` 144 | 145 | ## API 146 | 147 | ### xhr-mock 148 | 149 | #### .setup() 150 | 151 | Replace the global `XMLHttpRequest` object with the `MockXMLHttpRequest`. 152 | 153 | #### .teardown() 154 | 155 | Restore the global `XMLHttpRequest` object to its original state. 156 | 157 | #### .reset() 158 | 159 | Forget all the request handlers. 160 | 161 | #### .get(url | regex, mock) 162 | 163 | Register a factory function to create mock responses for each GET request to a specific URL. 164 | 165 | ```js 166 | mock.get(/\.*.json$/, { 167 | body: JSON.stringify({ data: { id: "abc" } }) 168 | }); 169 | ``` 170 | 171 | #### .post(url | regex, mock) 172 | 173 | Register a factory function to create mock responses for each POST request to a specific URL. 174 | 175 | #### .put(url | regex, mock) 176 | 177 | Register a factory function to create mock responses for each PUT request to a specific URL. 178 | 179 | #### .patch(url | regex, mock) 180 | 181 | Register a factory function to create mock responses for each PATCH request to a specific URL. 182 | 183 | #### .delete(url | regex, mock) 184 | 185 | Register a factory function to create mock responses for each DELETE request to a specific URL. 186 | 187 | #### .use(method, url | regex, mock) 188 | 189 | Register a factory function to create mock responses for each request to a specific URL. 190 | 191 | #### .use(fn) 192 | 193 | Register a factory function to create mock responses for every request. 194 | 195 | #### .error(fn) 196 | 197 | Log errors thrown by handlers. 198 | 199 | ### MockXMLHttpRequest 200 | 201 | ### MockRequest 202 | 203 | #### .method() : string 204 | 205 | Get the request method. 206 | 207 | #### .url() : MockURL 208 | 209 | Get the request URL. 210 | 211 | #### .header(name : string, value: string) 212 | 213 | Set a request header. 214 | 215 | #### .header(name : string) : string | null 216 | 217 | Get a request header. 218 | 219 | #### .headers() : object 220 | 221 | Get the request headers. 222 | 223 | #### .headers(headers : object) 224 | 225 | Set the request headers. 226 | 227 | #### .body() : string 228 | 229 | Get the request body. 230 | 231 | #### .body(body : string) 232 | 233 | Set the request body. 234 | 235 | ### MockResponse 236 | 237 | #### .status() : number 238 | 239 | Get the response status. 240 | 241 | #### .status(code : number) 242 | 243 | Set the response status. 244 | 245 | #### .reason() : string 246 | 247 | Get the response reason. 248 | 249 | #### .reason(phrase : string) 250 | 251 | Set the response reason. 252 | 253 | #### .header(name : string, value: string) 254 | 255 | Set a response header. 256 | 257 | #### .header(name : string) : string | null 258 | 259 | Get a response header. 260 | 261 | #### .headers() : object 262 | 263 | Get the response headers. 264 | 265 | #### .headers(headers : object) 266 | 267 | Set the response headers. 268 | 269 | #### .body() : string 270 | 271 | Get the response body. 272 | 273 | #### .body(body : string) 274 | 275 | Set the response body. 276 | 277 | ## How to? 278 | 279 | ### Simulate progress 280 | 281 | #### Upload progress 282 | 283 | Set the `Content-Length` header and send a body. `xhr-mock` will emit `ProgressEvent`s. 284 | 285 | ```js 286 | import mock from 'xhr-mock'; 287 | 288 | mock.setup(); 289 | 290 | mock.post('/', {}); 291 | 292 | const xhr = new XMLHttpRequest(); 293 | xhr.upload.onprogress = event => console.log(event.loaded, event.total); 294 | xhr.open('POST', '/'); 295 | xhr.setRequestHeader('Content-Length', '12'); 296 | xhr.send('Hello World!'); 297 | ``` 298 | 299 | #### Download progress 300 | 301 | Set the `Content-Length` header and send a body. `xhr-mock` will emit `ProgressEvent`s. 302 | 303 | ```js 304 | import mock from 'xhr-mock'; 305 | 306 | mock.setup(); 307 | 308 | mock.get('/', { 309 | headers: {'Content-Length': '12'}, 310 | body: 'Hello World!' 311 | }); 312 | 313 | const xhr = new XMLHttpRequest(); 314 | xhr.onprogress = event => console.log(event.loaded, event.total); 315 | xhr.open('GET', '/'); 316 | xhr.send(); 317 | ``` 318 | 319 | ### Simulate a timeout 320 | 321 | Return a `Promise` that never resolves or rejects. 322 | 323 | ```js 324 | import mock from 'xhr-mock'; 325 | 326 | mock.setup(); 327 | 328 | mock.get('/', () => new Promise(() => {})); 329 | 330 | const xhr = new XMLHttpRequest(); 331 | xhr.timeout = 100; 332 | xhr.ontimeout = event => console.log('timeout'); 333 | xhr.open('GET', '/'); 334 | xhr.send(); 335 | ``` 336 | 337 | > A number of major libraries don't use the `timeout` event and use `setTimeout()` instead. Therefore, in order to mock timeouts in major libraries, we have to wait for the specified amount of time anyway. 338 | 339 | ### Simulate an error 340 | 341 | Return a `Promise` that rejects. If you want to test a particular error you an use one of the pre-defined error classes. 342 | 343 | ```js 344 | import mock from 'xhr-mock'; 345 | 346 | mock.setup(); 347 | 348 | mock.get('/', () => Promise.reject(new Error())); 349 | 350 | const xhr = new XMLHttpRequest(); 351 | xhr.onerror = event => console.log('error'); 352 | xhr.open('GET', '/'); 353 | xhr.send(); 354 | ``` 355 | 356 | ### Proxying requests 357 | 358 | If you want to mock some requests, but not all of them, you can proxy unhandled requests to a real server. 359 | 360 | ```js 361 | import mock, {proxy} from 'xhr-mock'; 362 | 363 | mock.setup(); 364 | 365 | // mock specific requests 366 | mock.post('/', {status: 204}); 367 | 368 | // proxy unhandled requests to the real servers 369 | mock.use(proxy); 370 | 371 | // this request will receive a mocked response 372 | const xhr1 = new XMLHttpRequest(); 373 | xhr1.open('POST', '/'); 374 | xhr1.send(); 375 | 376 | // this request will receieve the real response 377 | const xhr2 = new XMLHttpRequest(); 378 | xhr2.open('GET', 'https://jsonplaceholder.typicode.com/users/1'); 379 | xhr2.send(); 380 | ``` 381 | 382 | ### Delaying requests 383 | 384 | Requests can be delayed using our handy `delay` utility. 385 | 386 | ```js 387 | import mock, {delay} from 'xhr-mock'; 388 | 389 | mock.setup(); 390 | 391 | // delay the request for three seconds 392 | mock.post('/', delay({status: 201}, 3000)); 393 | ``` 394 | 395 | ### Once off requests 396 | 397 | Requests can be made on one off occasions using our handy `once` utility. 398 | 399 | ```js 400 | import mock, {once} from 'xhr-mock'; 401 | 402 | mock.setup(); 403 | 404 | // the response will only be returned the first time a request is made 405 | mock.post('/', once({status: 201})); 406 | ``` 407 | 408 | ### send a sequence of responses 409 | 410 | In case you need to return a different response each time a request is made, you may use the `sequence` utility. 411 | 412 | ```js 413 | import mock, {sequence} from 'xhr-mock'; 414 | 415 | mock.setup(); 416 | 417 | mock.post('/', sequence([ 418 | {status: 200}, // the first request will receive a response with status 200 419 | {status: 500} // the second request will receive a response with status 500 420 | // if a third request is made, no response will be sent 421 | ] 422 | )); 423 | ``` 424 | 425 | ## License 426 | 427 | MIT Licensed. Copyright (c) James Newell 2014. 428 | -------------------------------------------------------------------------------- /packages/xhr-mock/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testURL: 'http://localhost/', 3 | testRegex: '(src|test)/.*\\.test\\.ts$', 4 | transform: { 5 | '^.+\\.tsx?$': 'ts-jest' 6 | }, 7 | moduleFileExtensions: ['ts', 'js', 'json'] 8 | }; 9 | -------------------------------------------------------------------------------- /packages/xhr-mock/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xhr-mock", 3 | "version": "2.5.1", 4 | "description": "Utility for mocking XMLHttpRequest.", 5 | "keywords": [ 6 | "mock", 7 | "xhr", 8 | "test", 9 | "fake", 10 | "request", 11 | "ajax", 12 | "browser", 13 | "xmlhttprequest", 14 | "jquery", 15 | "superagent", 16 | "axios" 17 | ], 18 | "repository": "jameslnewell/xhr-mock", 19 | "main": "lib/index.js", 20 | "typings": "lib/index.d.ts", 21 | "files": [ 22 | "lib", 23 | "dist" 24 | ], 25 | "browser": { 26 | "./lib/proxy.js": "./lib/proxy.browser.js" 27 | }, 28 | "dependencies": { 29 | "global": "^4.3.0", 30 | "url": "^0.11.0" 31 | }, 32 | "devDependencies": { 33 | "@alexlur/rollup-plugin-typescript": "^1.0.4", 34 | "@types/jest": "22.2.0", 35 | "@types/node": "^9.3.0", 36 | "axios": "^0.19.0", 37 | "jest": "^22.0.5", 38 | "rollup": "^0.53.3", 39 | "rollup-plugin-commonjs": "^8.2.6", 40 | "rollup-plugin-node-resolve": "^3.0.0", 41 | "ts-jest": "^22.0.1", 42 | "tslint": "^5.9.1", 43 | "typescript": "2.7.2" 44 | }, 45 | "scripts": { 46 | "clean": "rm -rf ./lib ./dist", 47 | "lint": "tslint -c ../../tslint.base.json '{src,test}/**/*.ts'", 48 | "check": "tsc --pretty --noEmit --project tsconfig.test.json", 49 | "build:cjs": "tsc --pretty --declaration", 50 | "build:cjs:dev": "tsc --pretty --declaration --watch", 51 | "build:umd": "rollup --config", 52 | "build": "yarn run build:cjs && yarn run build:umd", 53 | "test": "jest", 54 | "test:watch": "jest --watch", 55 | "ci": "yarn run clean && yarn run check && yarn run lint && yarn run build && yarn run test" 56 | }, 57 | "license": "MIT" 58 | } 59 | -------------------------------------------------------------------------------- /packages/xhr-mock/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@alexlur/rollup-plugin-typescript'; 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | import commonjs from 'rollup-plugin-commonjs'; 4 | 5 | export default { 6 | input: 'src/index.umd.ts', 7 | output: { 8 | file: 'dist/xhr-mock.js', 9 | format: 'umd', 10 | name: 'XHRMock', 11 | exports: 'default' 12 | }, 13 | plugins: [ 14 | typescript({typescript: require('typescript')}), 15 | resolve({ 16 | preferBuiltins: false 17 | }), 18 | commonjs() 19 | ] 20 | }; -------------------------------------------------------------------------------- /packages/xhr-mock/src/MockError.ts: -------------------------------------------------------------------------------- 1 | export class MockError extends Error { 2 | constructor(message: string) { 3 | super(message); 4 | // hack to make instanceof work @see https://stackoverflow.com/questions/31626231/custom-error-class-in-typescript 5 | Object.setPrototypeOf(this, MockError.prototype); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/MockEvent.ts: -------------------------------------------------------------------------------- 1 | export default class MockEvent implements Event { 2 | readonly bubbles: boolean; 3 | readonly cancelable: boolean; 4 | cancelBubble: boolean; 5 | readonly currentTarget: EventTarget; 6 | readonly defaultPrevented: boolean; 7 | readonly eventPhase: number = 0; 8 | readonly isTrusted: boolean; 9 | returnValue: boolean; 10 | readonly srcElement: Element | null; 11 | readonly target: EventTarget; 12 | readonly timeStamp: number; 13 | readonly type: string; 14 | readonly scoped: boolean; 15 | 16 | readonly AT_TARGET: number; 17 | readonly BUBBLING_PHASE: number; 18 | readonly CAPTURING_PHASE: number; 19 | 20 | constructor(type: string, eventInitDict?: EventInit) { 21 | this.type = type || ''; 22 | if (eventInitDict) { 23 | const { 24 | scoped = false, 25 | bubbles = false, 26 | cancelable = false 27 | } = eventInitDict; 28 | this.scoped = scoped; 29 | this.bubbles = bubbles; 30 | this.cancelable = cancelable; 31 | } 32 | } 33 | 34 | initEvent( 35 | eventTypeArg: string, 36 | canBubbleArg: boolean, 37 | cancelableArg: boolean 38 | ): void { 39 | throw new Error(); 40 | } 41 | 42 | preventDefault(): void { 43 | throw new Error(); 44 | } 45 | 46 | stopImmediatePropagation(): void { 47 | throw new Error(); 48 | } 49 | 50 | stopPropagation(): void { 51 | throw new Error(); 52 | } 53 | 54 | deepPath(): EventTarget[] { 55 | throw new Error(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/MockEventTarget.test.ts: -------------------------------------------------------------------------------- 1 | import MockEvent from './MockEvent'; 2 | import MockEventTarget from './MockEventTarget'; 3 | 4 | describe('MockEventTarget', () => { 5 | it('should call the listener once when added once', () => { 6 | const target = new MockEventTarget(); 7 | 8 | const listener = jest.fn(); 9 | target.addEventListener('event1', listener); 10 | 11 | target.dispatchEvent(new MockEvent('event1')); 12 | 13 | expect(listener.mock.calls).toHaveLength(1); 14 | }); 15 | 16 | it('should call the listener once when added multiple times', () => { 17 | const target = new MockEventTarget(); 18 | 19 | const listener = jest.fn(); 20 | target.addEventListener('event1', listener); 21 | target.addEventListener('event1', listener); 22 | 23 | target.dispatchEvent(new MockEvent('event1')); 24 | 25 | expect(listener.mock.calls).toHaveLength(1); 26 | }); 27 | 28 | it('should call the correct listener when multiple listeners are added', () => { 29 | const target = new MockEventTarget(); 30 | 31 | const listener1 = jest.fn(); 32 | target.addEventListener('event1', listener1); 33 | const listener2 = jest.fn(); 34 | target.addEventListener('event2', listener2); 35 | 36 | target.dispatchEvent(new MockEvent('event1')); 37 | 38 | expect(listener1.mock.calls).toHaveLength(1); 39 | expect(listener2.mock.calls).toHaveLength(0); 40 | }); 41 | 42 | it('should not call the listener when removed', () => { 43 | const target = new MockEventTarget(); 44 | 45 | const listener = jest.fn(); 46 | target.addEventListener('event1', listener); 47 | target.addEventListener('event1', listener); 48 | target.removeEventListener('event1', listener); 49 | 50 | target.dispatchEvent(new MockEvent('event1')); 51 | 52 | expect(listener.mock.calls).toHaveLength(0); 53 | }); 54 | 55 | it('should set this', () => { 56 | expect.assertions(1); 57 | const target = new MockEventTarget(); 58 | 59 | target.addEventListener('event1', function(event) { 60 | expect(this).toBe(target); 61 | }); 62 | 63 | target.dispatchEvent(new MockEvent('event1')); 64 | }); 65 | 66 | it('should set the target and currentTarget', () => { 67 | expect.assertions(2); 68 | const target = new MockEventTarget(); 69 | 70 | target.addEventListener('event1', event => { 71 | expect(event.target).toBe(target); 72 | expect(event.currentTarget).toBe(target); 73 | }); 74 | 75 | target.dispatchEvent(new MockEvent('event1')); 76 | }); 77 | 78 | it('should set this when an `onevent1` is called', () => { 79 | expect.assertions(1); 80 | const target = new MockEventTarget(); 81 | 82 | (target as any).onevent1 = function(event: MockEvent) { 83 | expect(this).toBe(target); 84 | }; 85 | 86 | target.dispatchEvent(new MockEvent('event1')); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/MockEventTarget.ts: -------------------------------------------------------------------------------- 1 | import MockEvent from './MockEvent'; 2 | 3 | export default class MockEventTarget implements EventTarget { 4 | private listeners?: { 5 | [type: string]: EventListenerOrEventListenerObject[]; 6 | } = {}; 7 | 8 | addEventListener( 9 | type: string, 10 | listener?: EventListenerOrEventListenerObject, 11 | options?: boolean | AddEventListenerOptions 12 | ): void { 13 | this.listeners = this.listeners || {}; 14 | 15 | if (!listener) { 16 | return; 17 | } 18 | 19 | if (!this.listeners[type]) { 20 | this.listeners[type] = []; 21 | } 22 | 23 | //handleEvent 24 | if (this.listeners[type].indexOf(listener) === -1) { 25 | this.listeners[type].push(listener); 26 | } 27 | } 28 | 29 | removeEventListener( 30 | type: string, 31 | listener?: EventListenerOrEventListenerObject, 32 | options?: boolean | EventListenerOptions 33 | ): void { 34 | this.listeners = this.listeners || {}; 35 | 36 | if (!listener) { 37 | return; 38 | } 39 | 40 | if (!this.listeners[type]) { 41 | return; 42 | } 43 | 44 | const index = this.listeners[type].indexOf(listener); 45 | if (index !== -1) { 46 | this.listeners[type].splice(index, 1); 47 | } 48 | } 49 | 50 | dispatchEvent(event: Event): boolean { 51 | this.listeners = this.listeners || {}; 52 | 53 | //set the event target 54 | (event as any).target = this; 55 | (event as any).currentTarget = this; 56 | 57 | //call any built-in listeners 58 | //FIXME: the listener should be added on set 59 | const method = (this as any)[`on${event.type}`]; 60 | if (method) { 61 | method.call(this, event); 62 | } 63 | 64 | if (!this.listeners[event.type]) { 65 | return true; 66 | } 67 | 68 | this.listeners[event.type].forEach(listener => { 69 | if (typeof listener === 'function') { 70 | listener.call(this, event); 71 | } else { 72 | listener.handleEvent.call(this, event); 73 | } 74 | }); 75 | return true; //TODO: return type based on .cancellable and .preventDefault() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/MockHeaders.ts: -------------------------------------------------------------------------------- 1 | export type MockHeaders = {[name: string]: string}; 2 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/MockProgressEvent.ts: -------------------------------------------------------------------------------- 1 | import MockEvent from './MockEvent'; 2 | 3 | export default class MockProgressEvent extends MockEvent 4 | implements ProgressEvent { 5 | readonly lengthComputable: boolean; 6 | readonly loaded: number; 7 | readonly total: number; 8 | 9 | constructor(type: string, eventInitDict?: ProgressEventInit) { 10 | super(type, eventInitDict); 11 | if (eventInitDict) { 12 | const {lengthComputable = false, loaded = 0, total = 0} = eventInitDict; 13 | this.lengthComputable = lengthComputable; 14 | this.loaded = loaded; 15 | this.total = total; 16 | } 17 | } 18 | 19 | initProgressEvent( 20 | typeArg: string, 21 | canBubbleArg: boolean, 22 | cancelableArg: boolean, 23 | lengthComputableArg: boolean, 24 | loadedArg: number, 25 | totalArg: number 26 | ): void { 27 | throw new Error(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/MockRequest.test.ts: -------------------------------------------------------------------------------- 1 | import MockEventTarget from './MockEventTarget'; 2 | import MockRequest from './MockRequest'; 3 | 4 | describe('MockRequest', function() { 5 | describe('.method()', () => { 6 | it('should be an empty string when not set', () => { 7 | const req = new MockRequest(); 8 | expect(req.method()).toEqual('GET'); 9 | }); 10 | 11 | it('should be post when set', () => { 12 | const req = new MockRequest(); 13 | req.method('POST'); 14 | expect(req.method()).toEqual('POST'); 15 | }); 16 | 17 | it('should be uppercase when set', () => { 18 | const req = new MockRequest(); 19 | req.method('put'); 20 | expect(req.method()).toEqual('PUT'); 21 | }); 22 | 23 | it('should be chicken when set', () => { 24 | const req = new MockRequest(); 25 | req.method('chicken'); 26 | expect(req.method()).toEqual('chicken'); 27 | }); 28 | 29 | it('should return the request when the value is set', () => { 30 | const req = new MockRequest(); 31 | expect(req.method('PUT')).toBe(req); 32 | }); 33 | }); 34 | 35 | describe('.url()', () => { 36 | it('should be an empty object when not set', () => { 37 | const req = new MockRequest(); 38 | expect(req.url()).toEqual({}); 39 | }); 40 | 41 | it('should be a URL when set', () => { 42 | const req = new MockRequest(); 43 | req.url('http://www.example.com/test.php?a=1&b=2'); 44 | expect(req.url()).toEqual( 45 | expect.objectContaining({ 46 | protocol: 'http', 47 | host: 'www.example.com', 48 | path: '/test.php', 49 | query: {a: '1', b: '2'} 50 | }) 51 | ); 52 | expect(req.url().toString()).toEqual( 53 | 'http://www.example.com/test.php?a=1&b=2' 54 | ); 55 | }); 56 | 57 | it('should return the request when the value is set', () => { 58 | const req = new MockRequest(); 59 | expect(req.url('http://www.example.com/')).toBe(req); 60 | }); 61 | }); 62 | 63 | describe('.header()', () => { 64 | it('should be null when not set', () => { 65 | const req = new MockRequest(); 66 | expect(req.header('content-type')).toEqual(null); 67 | }); 68 | 69 | it('should be image/jpeg when set', () => { 70 | const req = new MockRequest(); 71 | req.header('content-type', 'image/jpeg'); 72 | expect(req.header('content-type')).toEqual('image/jpeg'); 73 | }); 74 | 75 | it('should return the request when the value is set', () => { 76 | const req = new MockRequest(); 77 | expect(req.header('content-type', 'image/jpeg')).toBe(req); 78 | }); 79 | }); 80 | 81 | describe('.headers()', () => { 82 | it('should be an empty object when not set', () => { 83 | const req = new MockRequest(); 84 | expect(req.headers()).toEqual({}); 85 | }); 86 | 87 | it('should be an empty object when not set', () => { 88 | const req = new MockRequest(); 89 | req.headers({'content-type': 'image/jpeg'}); 90 | expect(req.headers()).toEqual( 91 | expect.objectContaining({'content-type': 'image/jpeg'}) 92 | ); 93 | }); 94 | 95 | it('should return the request when the value is set', () => { 96 | const req = new MockRequest(); 97 | expect(req.headers({'content-type': 'image/jpeg'})).toBe(req); 98 | }); 99 | }); 100 | 101 | describe('.body()', () => { 102 | it('should be null when not set', () => { 103 | const req = new MockRequest(); 104 | expect(req.body()).toEqual(null); 105 | }); 106 | 107 | it('should be HelloWorld when set', () => { 108 | const req = new MockRequest(); 109 | req.body('HelloWorld'); 110 | expect(req.body()).toEqual('HelloWorld'); 111 | }); 112 | 113 | it('should return the request when the value is set', () => { 114 | const req = new MockRequest(); 115 | expect(req.body('HelloWorld')).toBe(req); 116 | }); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/MockRequest.ts: -------------------------------------------------------------------------------- 1 | import {MockURL, parseURL} from './MockURL'; 2 | import {MockHeaders} from './MockHeaders'; 3 | import EventTarget from './MockEventTarget'; 4 | import MockProgressEvent from './MockProgressEvent'; 5 | 6 | const FORBIDDEN_METHODS = ['CONNECT', 'TRACE', 'TRACK']; 7 | const UPPERCASE_METHODS = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']; 8 | 9 | export default class MockRequest { 10 | private _method: string = 'GET'; 11 | private _url: MockURL = parseURL(''); 12 | private _headers: MockHeaders = {}; 13 | private _body: null | string = null; 14 | 15 | method(): string; 16 | method(method: string): MockRequest; 17 | method(method?: string): string | MockRequest { 18 | if (typeof method !== 'undefined') { 19 | if (FORBIDDEN_METHODS.indexOf(method.toUpperCase()) !== -1) { 20 | throw new Error(`xhr-mock: Method "${method}" is forbidden.`); 21 | } 22 | 23 | if (UPPERCASE_METHODS.indexOf(method.toUpperCase()) !== -1) { 24 | this._method = method.toUpperCase(); 25 | } else { 26 | this._method = method; 27 | } 28 | 29 | return this; 30 | } else { 31 | return this._method; 32 | } 33 | } 34 | 35 | url(): MockURL; 36 | url(url: string): MockRequest; 37 | url(url?: string): MockURL | MockRequest { 38 | if (typeof url === 'string') { 39 | this._url = parseURL(url); 40 | return this; 41 | } else { 42 | return this._url; 43 | } 44 | } 45 | 46 | header(name: string): null | string; 47 | header(name: string, value: string): MockRequest; 48 | header(name: string, value?: string): null | string | MockRequest { 49 | if (typeof value !== 'undefined') { 50 | this._headers[name.toLowerCase()] = value; 51 | return this; 52 | } else { 53 | return this._headers[name.toLowerCase()] || null; 54 | } 55 | } 56 | 57 | headers(): MockHeaders; 58 | headers(headers: MockHeaders): MockRequest; 59 | headers(headers?: MockHeaders): MockHeaders | MockRequest { 60 | if (typeof headers === 'object') { 61 | for (let name in headers) { 62 | if (headers.hasOwnProperty(name)) { 63 | this.header(name, headers[name]); 64 | } 65 | } 66 | return this; 67 | } else { 68 | return this._headers; 69 | } 70 | } 71 | 72 | body(): any; 73 | body(body: any): MockRequest; 74 | body(body?: any): any | MockRequest { 75 | if (typeof body !== 'undefined') { 76 | this._body = body; 77 | return this; 78 | } else { 79 | return this._body; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/MockResponse.test.ts: -------------------------------------------------------------------------------- 1 | import MockEventTarget from './MockEventTarget'; 2 | import MockResponse from './MockResponse'; 3 | 4 | describe('MockResponse', () => { 5 | describe('.status()', () => { 6 | it('should be 200 when not set', () => { 7 | const res = new MockResponse(); 8 | expect(res.status()).toEqual(200); 9 | }); 10 | 11 | it('should be 404 when set', () => { 12 | const res = new MockResponse(); 13 | res.status(404); 14 | expect(res.status()).toEqual(404); 15 | }); 16 | 17 | it('should return the response when the value is set', () => { 18 | const res = new MockResponse(); 19 | expect(res.status(404)).toBe(res); 20 | }); 21 | }); 22 | 23 | describe('.reason()', () => { 24 | it('should be OK when not set', () => { 25 | const res = new MockResponse(); 26 | expect(res.reason()).toEqual('OK'); 27 | }); 28 | 29 | it('should be Not found when set', () => { 30 | const res = new MockResponse(); 31 | res.reason('Not found'); 32 | expect(res.reason()).toEqual('Not found'); 33 | }); 34 | 35 | it('should return the response when the value is set', () => { 36 | const res = new MockResponse(); 37 | expect(res.reason('Not found')).toBe(res); 38 | }); 39 | }); 40 | 41 | describe('.header()', () => { 42 | it('should be null when not set', () => { 43 | const res = new MockResponse(); 44 | expect(res.header('content-type')).toEqual(null); 45 | }); 46 | 47 | it('should be image/jpeg when set', () => { 48 | const res = new MockResponse(); 49 | res.header('content-type', 'image/jpeg'); 50 | expect(res.header('content-type')).toEqual('image/jpeg'); 51 | }); 52 | 53 | it('should return the response when the value is set', () => { 54 | const res = new MockResponse(); 55 | expect(res.header('content-type', 'image/jpeg')).toBe(res); 56 | }); 57 | }); 58 | 59 | describe('.headers()', () => { 60 | it('should be an empty object when not set', () => { 61 | const res = new MockResponse(); 62 | expect(res.headers()).toEqual({}); 63 | }); 64 | 65 | it('should be an empty object when not set', () => { 66 | const res = new MockResponse(); 67 | res.headers({'content-type': 'image/jpeg'}); 68 | expect(res.headers()).toEqual( 69 | expect.objectContaining({'content-type': 'image/jpeg'}) 70 | ); 71 | }); 72 | 73 | it('should return the response when the value is set', () => { 74 | const res = new MockResponse(); 75 | expect(res.headers({'content-type': 'image/jpeg'})).toBe(res); 76 | }); 77 | }); 78 | 79 | describe('.body()', () => { 80 | it('should be an empty string when not set', () => { 81 | const res = new MockResponse(); 82 | res.body('xyz'); 83 | expect(res.body()).toEqual('xyz'); 84 | }); 85 | 86 | it('should be HelloWorld when set', () => { 87 | const res = new MockResponse(); 88 | res.body('HelloWorld'); 89 | expect(res.body()).toEqual('HelloWorld'); 90 | }); 91 | 92 | it('should return the response when the value is set', () => { 93 | const res = new MockResponse(); 94 | expect(res.body('HelloWorld')).toBe(res); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/MockResponse.ts: -------------------------------------------------------------------------------- 1 | import {MockHeaders} from './MockHeaders'; 2 | import EventTarget from './MockEventTarget'; 3 | import MockProgressEvent from './MockProgressEvent'; 4 | 5 | export default class MockResponse { 6 | private _status: number = 200; 7 | private _reason: string = 'OK'; 8 | private _headers: MockHeaders = {}; 9 | private _body: any = null; 10 | 11 | status(): number; 12 | status(status: number): MockResponse; 13 | status(status?: number): number | MockResponse { 14 | if (typeof status !== 'undefined') { 15 | this._status = status; 16 | return this; 17 | } else { 18 | return this._status; 19 | } 20 | } 21 | 22 | reason(): string; 23 | reason(reason: string): MockResponse; 24 | reason(reason?: string): string | MockResponse { 25 | if (typeof reason !== 'undefined') { 26 | this._reason = reason; 27 | return this; 28 | } else { 29 | return this._reason; 30 | } 31 | } 32 | 33 | statusText(): null | string; 34 | statusText(reason: string): MockResponse; 35 | statusText(reason?: string): null | string | MockResponse { 36 | console.warn( 37 | 'xhr-mock: MockResponse.statusText() has been deprecated. Use MockResponse.reason() instead.' 38 | ); 39 | if (typeof reason !== 'undefined') { 40 | return this.reason(reason); 41 | } else { 42 | return this.reason(); 43 | } 44 | } 45 | 46 | header(name: string): null | string; 47 | header(name: string, value: string): MockResponse; 48 | header(name: string, value?: string): null | string | MockResponse { 49 | if (typeof value !== 'undefined') { 50 | this._headers[name.toLowerCase()] = value; 51 | return this; 52 | } else { 53 | return this._headers[name.toLowerCase()] || null; 54 | } 55 | } 56 | 57 | headers(): MockHeaders; 58 | headers(headers: MockHeaders): MockResponse; 59 | headers(headers?: MockHeaders): MockHeaders | MockResponse { 60 | if (typeof headers === 'object') { 61 | for (let name in headers) { 62 | if (headers.hasOwnProperty(name)) { 63 | this.header(name, headers[name]); 64 | } 65 | } 66 | return this; 67 | } else { 68 | return this._headers; 69 | } 70 | } 71 | 72 | body(): any; 73 | body(body: any): MockResponse; 74 | body(body?: any): any | MockResponse { 75 | if (typeof body !== 'undefined') { 76 | this._body = body; 77 | return this; 78 | } else { 79 | return this._body; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/MockURL.test.ts: -------------------------------------------------------------------------------- 1 | import {MockURL, parseURL, formatURL} from './MockURL'; 2 | 3 | const valid: {[key: string]: MockURL} = { 4 | 'http://james@localhost:8080/api/user/123?foo=bar': { 5 | protocol: 'http', 6 | username: 'james', 7 | host: 'localhost', 8 | port: 8080, 9 | path: '/api/user/123', 10 | query: { 11 | foo: 'bar' 12 | } 13 | }, 14 | 15 | 'http://james:abc123@localhost:8080/api/user/123?foo=bar': { 16 | protocol: 'http', 17 | username: 'james', 18 | password: 'abc123', 19 | host: 'localhost', 20 | port: 8080, 21 | path: '/api/user/123', 22 | query: { 23 | foo: 'bar' 24 | } 25 | }, 26 | 27 | 'http://localhost:8080/api/user/123?foo=bar': { 28 | protocol: 'http', 29 | host: 'localhost', 30 | port: 8080, 31 | path: '/api/user/123', 32 | query: { 33 | foo: 'bar' 34 | } 35 | }, 36 | 37 | '/api/user/123?foo=bar': { 38 | path: '/api/user/123', 39 | query: { 40 | foo: 'bar' 41 | } 42 | } 43 | }; 44 | 45 | describe('MockURL', () => { 46 | describe('parseURL()', () => { 47 | it('should return a valid object', () => { 48 | Object.keys(valid).forEach(url => { 49 | expect(parseURL(url)).toEqual(valid[url]); 50 | }); 51 | }); 52 | }); 53 | 54 | describe('formatURL()', () => { 55 | it('should return a valid URL', () => { 56 | Object.keys(valid).forEach(url => { 57 | expect(formatURL(valid[url])).toEqual(url); 58 | }); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/MockURL.ts: -------------------------------------------------------------------------------- 1 | import {parse, format} from 'url'; 2 | 3 | export interface MockURL { 4 | protocol?: string; 5 | username?: string; 6 | password?: string; 7 | host?: string; 8 | port?: number; 9 | path?: string; 10 | query?: {[name: string]: string}; 11 | hash?: string; 12 | toString(): string; 13 | } 14 | 15 | // put toString() in a class so it isn't included in the props when checked for equality 16 | class MockURLImplementation implements MockURL { 17 | toString(): string { 18 | return formatURL(this); 19 | } 20 | } 21 | 22 | export function parseURL(url: string): MockURL { 23 | const urlObject: MockURL = new MockURLImplementation(); 24 | 25 | if (!url) { 26 | return urlObject; 27 | } 28 | 29 | const parsedURL = parse(url, true); 30 | 31 | if (parsedURL.protocol) { 32 | urlObject.protocol = parsedURL.protocol.substr( 33 | 0, 34 | parsedURL.protocol.length - 1 35 | ); 36 | } 37 | 38 | if (parsedURL.auth) { 39 | const [username, password] = parsedURL.auth.split(':'); 40 | if (username && password) { 41 | urlObject.username = username; 42 | urlObject.password = password; 43 | } else { 44 | urlObject.username = username; 45 | } 46 | } 47 | 48 | if (parsedURL.hostname) { 49 | urlObject.host = parsedURL.hostname; 50 | } 51 | 52 | if (parsedURL.port) { 53 | urlObject.port = parseInt(parsedURL.port, 10); 54 | } 55 | 56 | if (parsedURL.pathname) { 57 | urlObject.path = parsedURL.pathname; 58 | } 59 | 60 | if (parsedURL.query) { 61 | urlObject.query = parsedURL.query; 62 | } 63 | 64 | if (parsedURL.hash) { 65 | urlObject.hash = parsedURL.hash; 66 | } 67 | 68 | return urlObject; 69 | } 70 | 71 | export function formatURL(url: MockURL): string { 72 | const obj = { 73 | protocol: url.protocol, 74 | auth: 75 | url.username && url.password 76 | ? `${url.username}:${url.password}` 77 | : url.username, 78 | hostname: url.host, 79 | port: typeof url.port === 'number' ? String(url.port) : url.port, 80 | pathname: url.path, 81 | query: url.query, 82 | hash: url.hash 83 | }; 84 | return format(obj); 85 | } 86 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/MockXMLHttpRequest.test.ts: -------------------------------------------------------------------------------- 1 | import MockEvent from './MockEvent'; 2 | import MockProgressEvent from './MockProgressEvent'; 3 | import MockXMLHttpRequest from './MockXMLHttpRequest'; 4 | import {MockRequest} from '.'; 5 | import {MockError} from './MockError'; 6 | 7 | function failOnEvent(done: jest.DoneCallback) { 8 | return function(event: MockProgressEvent) { 9 | done.fail(); 10 | }; 11 | } 12 | 13 | function successOnEvent(done: jest.DoneCallback) { 14 | return function(event: MockProgressEvent) { 15 | done(); 16 | }; 17 | } 18 | 19 | function addListeners(xhr: MockXMLHttpRequest, events: string[]) { 20 | const pushEvent = (event: MockEvent) => events.push(`xhr:${event.type}`); 21 | xhr.addEventListener('readystatechange', pushEvent); 22 | xhr.addEventListener('loadstart', pushEvent); 23 | xhr.addEventListener('progress', pushEvent); 24 | xhr.addEventListener('load', pushEvent); 25 | xhr.addEventListener('loadend', pushEvent); 26 | 27 | const uploadPushEvent = (event: MockEvent) => 28 | events.push(`upload:${event.type}`); 29 | xhr.upload.addEventListener('loadstart', uploadPushEvent); 30 | xhr.upload.addEventListener('progress', uploadPushEvent); 31 | xhr.upload.addEventListener('load', uploadPushEvent); 32 | xhr.upload.addEventListener('loadend', uploadPushEvent); 33 | } 34 | 35 | describe('MockXMLHttpRequest', () => { 36 | beforeEach(() => { 37 | MockXMLHttpRequest.removeAllHandlers(); 38 | MockXMLHttpRequest.errorCallback = () => { 39 | /* do nothing */ 40 | }; 41 | }); 42 | 43 | describe('.response', () => { 44 | it('should return an empty string when type is empty string and the request is not loading and is not done', () => { 45 | const xhr = new MockXMLHttpRequest(); 46 | xhr.responseType = ''; 47 | xhr.open('get', '/'); 48 | expect(xhr.response).toEqual(''); 49 | }); 50 | 51 | it('should return an empty string when type is text and the request is not loading and is not done', () => { 52 | const xhr = new MockXMLHttpRequest(); 53 | xhr.responseType = 'text'; 54 | xhr.open('get', '/'); 55 | expect(xhr.response).toEqual(''); 56 | }); 57 | 58 | it('should return the responseText when type is empty string and the request is done', done => { 59 | MockXMLHttpRequest.addHandler((req, res) => res.body('Hello World!')); 60 | const xhr = new MockXMLHttpRequest(); 61 | xhr.responseType = ''; 62 | xhr.onload = () => { 63 | expect(xhr.response).toEqual('Hello World!'); 64 | done(); 65 | }; 66 | xhr.onerror = failOnEvent(done); 67 | xhr.open('get', '/'); 68 | xhr.send(); 69 | }); 70 | 71 | it('should return the responseText when type is text and the request is done', done => { 72 | MockXMLHttpRequest.addHandler((req, res) => res.body('Hello World!')); 73 | const xhr = new MockXMLHttpRequest(); 74 | xhr.responseType = ''; 75 | xhr.onload = () => { 76 | expect(xhr.response).toEqual('Hello World!'); 77 | done(); 78 | }; 79 | xhr.onerror = failOnEvent(done); 80 | xhr.open('get', '/'); 81 | xhr.send(); 82 | }); 83 | 84 | it('should return null when type is json and the request is not done', () => { 85 | MockXMLHttpRequest.addHandler((req, res) => res.body('{}')); 86 | const xhr = new MockXMLHttpRequest(); 87 | xhr.responseType = 'json'; 88 | xhr.open('get', '/'); 89 | expect(xhr.response).toEqual(null); 90 | }); 91 | 92 | it('should return json when the type is json and the request is done', done => { 93 | MockXMLHttpRequest.addHandler((req, res) => res.body('{"foo": "bar"}')); 94 | const xhr = new MockXMLHttpRequest(); 95 | xhr.responseType = 'json'; 96 | xhr.onload = () => { 97 | try { 98 | expect(xhr.response).toEqual({foo: 'bar'}); 99 | done(); 100 | } catch (error) { 101 | done.fail(error); 102 | } 103 | }; 104 | xhr.onerror = failOnEvent(done); 105 | xhr.open('get', '/'); 106 | xhr.send(); 107 | }); 108 | 109 | it('should return null when the type is other and the request is not done', () => { 110 | const fakeBuffer = {}; 111 | MockXMLHttpRequest.addHandler((req, res) => res.body(fakeBuffer)); 112 | const xhr = new MockXMLHttpRequest(); 113 | xhr.responseType = 'blob'; 114 | xhr.open('get', '/'); 115 | expect(xhr.response).toEqual(null); 116 | }); 117 | 118 | it('should return an object when the type is other and the request is done', done => { 119 | const fakeBuffer = {}; 120 | MockXMLHttpRequest.addHandler((req, res) => res.body(fakeBuffer)); 121 | const xhr = new MockXMLHttpRequest(); 122 | xhr.responseType = 'blob'; 123 | xhr.onload = () => { 124 | try { 125 | expect(xhr.response).toBe(fakeBuffer); 126 | done(); 127 | } catch (error) { 128 | done.fail(error); 129 | } 130 | }; 131 | xhr.onerror = failOnEvent(done); 132 | xhr.open('get', '/'); 133 | xhr.send(); 134 | }); 135 | }); 136 | 137 | describe('.setRequestHeader()', () => { 138 | it('should set a header', done => { 139 | expect.assertions(1); 140 | 141 | MockXMLHttpRequest.addHandler((req, res) => { 142 | expect(req.header('content-type')).toEqual('application/json'); 143 | return res; 144 | }); 145 | 146 | const xhr = new MockXMLHttpRequest(); 147 | xhr.open('GET', '/'); 148 | xhr.setRequestHeader('Content-Type', 'application/json'); 149 | xhr.onload = successOnEvent(done); 150 | xhr.onerror = failOnEvent(done); 151 | xhr.send(); 152 | }); 153 | }); 154 | 155 | describe('.getResponseHeader()', () => { 156 | it('should have a response header', done => { 157 | expect.assertions(1); 158 | MockXMLHttpRequest.addHandler((req, res) => { 159 | return res.header('Content-Type', 'application/json'); 160 | }); 161 | 162 | const xhr = new MockXMLHttpRequest(); 163 | xhr.open('get', '/'); 164 | xhr.onloadend = () => { 165 | expect(xhr.getResponseHeader('Content-Type')).toEqual( 166 | 'application/json' 167 | ); 168 | done(); 169 | }; 170 | xhr.onerror = failOnEvent(done); 171 | xhr.send(); 172 | }); 173 | }); 174 | 175 | describe('.getAllResponseHeaders()', () => { 176 | it('should have return headers as a string', done => { 177 | MockXMLHttpRequest.addHandler((req, res) => { 178 | return res 179 | .header('Content-Type', 'application/json') 180 | .header('X-Powered-By', 'SecretSauce'); 181 | }); 182 | 183 | const xhr = new MockXMLHttpRequest(); 184 | xhr.open('get', '/'); 185 | xhr.onload = () => { 186 | expect(xhr.getAllResponseHeaders()).toEqual( 187 | 'content-type: application/json\r\nx-powered-by: SecretSauce\r\n' 188 | ); 189 | done(); 190 | }; 191 | xhr.onerror = failOnEvent(done); 192 | xhr.send(); 193 | }); 194 | }); 195 | 196 | describe('.constructor()', () => { 197 | it('should set .readyState to UNSENT', () => { 198 | const xhr = new MockXMLHttpRequest(); 199 | expect(xhr.readyState).toEqual(MockXMLHttpRequest.UNSENT); 200 | }); 201 | }); 202 | 203 | describe('.open()', () => { 204 | it('should set .readyState to OPEN', () => { 205 | const xhr = new MockXMLHttpRequest(); 206 | xhr.open('get', '/'); 207 | expect(xhr.readyState).toEqual(MockXMLHttpRequest.OPENED); 208 | }); 209 | 210 | it('should call .onreadystatechange', () => { 211 | const callback = jest.fn(); 212 | const xhr = new MockXMLHttpRequest(); 213 | xhr.onreadystatechange = callback; 214 | xhr.open('get', '/'); 215 | expect(callback).toHaveBeenCalledTimes(1); //FIXME: check event 216 | }); 217 | }); 218 | 219 | describe('.send()', () => { 220 | it('should throw an error when .open() has not been called', () => { 221 | const xhr = new MockXMLHttpRequest(); 222 | expect(() => xhr.send()).toThrow(); 223 | }); 224 | 225 | describe('async=false', () => { 226 | it('should dispatch events in order when both the request and response do not contain a body', () => { 227 | MockXMLHttpRequest.addHandler((req, res) => res); 228 | const events: string[] = []; 229 | const xhr = new MockXMLHttpRequest(); 230 | xhr.open('get', '/', false); 231 | addListeners(xhr, events); 232 | xhr.send(); 233 | expect(events).toEqual([ 234 | 'xhr:readystatechange', //DONE 235 | 'xhr:load', 236 | 'xhr:loadend' 237 | ]); 238 | }); 239 | 240 | it('should dispatch events in order when request has a body', () => { 241 | MockXMLHttpRequest.addHandler((req, res) => res); 242 | const events: string[] = []; 243 | const xhr = new MockXMLHttpRequest(); 244 | xhr.open('put', '/', false); 245 | addListeners(xhr, events); 246 | xhr.send('hello world!'); 247 | expect(events).toEqual([ 248 | 'xhr:readystatechange', //DONE 249 | 'xhr:load', 250 | 'xhr:loadend' 251 | ]); 252 | }); 253 | 254 | it('should dispatch events in order when response has a body', () => { 255 | MockXMLHttpRequest.addHandler((req, res) => res.body('Hello World!')); 256 | const events: string[] = []; 257 | const xhr = new MockXMLHttpRequest(); 258 | xhr.open('put', '/', false); 259 | addListeners(xhr, events); 260 | xhr.send(); 261 | expect(events).toEqual([ 262 | 'xhr:readystatechange', //DONE 263 | 'xhr:load', 264 | 'xhr:loadend' 265 | ]); 266 | }); 267 | 268 | it('should call the error callback when there is an error', () => { 269 | expect.assertions(2); 270 | MockXMLHttpRequest.addHandler((req, res) => { 271 | throw new Error('test!'); 272 | }); 273 | MockXMLHttpRequest.errorCallback = ({req, err}) => { 274 | expect(req).toBeInstanceOf(MockRequest); 275 | expect(err).toBeInstanceOf(Error); 276 | }; 277 | 278 | try { 279 | const xhr = new MockXMLHttpRequest(); 280 | xhr.open('get', '/', false); 281 | xhr.send(); 282 | } catch (error) {} 283 | }); 284 | }); 285 | }); 286 | 287 | describe('async=true', () => { 288 | it('should dispatch events in order when both the request and response do not contain a body', done => { 289 | MockXMLHttpRequest.addHandler((req, res) => res); 290 | 291 | const events: string[] = []; 292 | const xhr = new MockXMLHttpRequest(); 293 | xhr.open('get', '/'); 294 | addListeners(xhr, events); 295 | xhr.onloadend = () => { 296 | expect(events).toEqual([ 297 | 'xhr:loadstart', 298 | 'xhr:readystatechange', //HEADERS_RECEIVED 299 | 'xhr:progress', 300 | 'xhr:readystatechange', //DONE 301 | 'xhr:load' 302 | ]); 303 | done(); 304 | }; 305 | xhr.send(); 306 | }); 307 | 308 | it('should dispatch events in order when request has a body', done => { 309 | MockXMLHttpRequest.addHandler((req, res) => res); 310 | 311 | const events: string[] = []; 312 | const xhr = new MockXMLHttpRequest(); 313 | xhr.open('put', '/'); 314 | addListeners(xhr, events); 315 | xhr.onloadend = () => { 316 | expect(events).toEqual([ 317 | 'xhr:loadstart', 318 | 'upload:loadstart', 319 | 'upload:progress', 320 | 'upload:load', 321 | 'upload:loadend', 322 | 'xhr:readystatechange', //HEADERS_RECEIVED 323 | 'xhr:progress', 324 | 'xhr:readystatechange', //DONE 325 | 'xhr:load' 326 | ]); 327 | done(); 328 | }; 329 | xhr.send('hello world!'); 330 | }); 331 | 332 | it('should dispatch events in order when response has a body', done => { 333 | MockXMLHttpRequest.addHandler((req, res) => res.body('Hello World!')); 334 | 335 | const events: string[] = []; 336 | const xhr = new MockXMLHttpRequest(); 337 | xhr.open('put', '/'); 338 | addListeners(xhr, events); 339 | xhr.onloadend = () => { 340 | expect(events).toEqual([ 341 | 'xhr:loadstart', 342 | 'xhr:readystatechange', //HEADERS_RECEIVED 343 | 'xhr:readystatechange', //LOADING 344 | 'xhr:progress', 345 | 'xhr:readystatechange', //DONE 346 | 'xhr:load' 347 | ]); 348 | done(); 349 | }; 350 | xhr.send(); 351 | }); 352 | }); 353 | 354 | describe('responseXML', () => { 355 | it('Should return null if status is not DONE', function() { 356 | const xhr = new MockXMLHttpRequest(); 357 | xhr.responseType = ''; 358 | xhr.open('get', '/'); 359 | expect(xhr.readyState).not.toBe(4); 360 | expect(xhr.responseXML).toEqual(null); 361 | }); 362 | 363 | it('Should return null if status is DONE and body type is not Document', function() { 364 | const xhr = new MockXMLHttpRequest(); 365 | xhr.responseType = ''; 366 | 367 | xhr.onload = () => { 368 | expect(xhr.readyState).toEqual(4); 369 | expect(xhr.responseXML).toBe(null); 370 | }; 371 | xhr.open('get', '/'); 372 | xhr.send(); 373 | }); 374 | 375 | it('Should return the document response if status is DONE and body type is Document', function(done) { 376 | const xml = ` 377 | `; 378 | 379 | const parser = new window.DOMParser(); 380 | const xmlDoc = parser.parseFromString(xml, 'application/xml'); 381 | 382 | MockXMLHttpRequest.addHandler((req, res) => res.body(xmlDoc)); 383 | 384 | const xhr = new MockXMLHttpRequest(); 385 | xhr.responseType = ''; 386 | 387 | xhr.onload = () => { 388 | try { 389 | expect(xhr.responseXML).toEqual(xmlDoc); 390 | done(); 391 | } catch (error) { 392 | done.fail(error); 393 | } 394 | }; 395 | xhr.open('get', '/'); 396 | xhr.send(); 397 | }); 398 | }); 399 | //TODO: check values of all events 400 | 401 | it('should set the request body when .send() is called with a body', done => { 402 | MockXMLHttpRequest.addHandler((req, res) => { 403 | expect(req.body()).toEqual('Hello World!'); 404 | return res; 405 | }); 406 | 407 | const xhr = new MockXMLHttpRequest(); 408 | xhr.onload = successOnEvent(done); 409 | xhr.onerror = failOnEvent(done); 410 | xhr.open('post', '/'); 411 | xhr.send('Hello World!'); 412 | }); 413 | 414 | it('should not set the request body when .send() is not called with a body', done => { 415 | MockXMLHttpRequest.addHandler((req, res) => { 416 | expect(req.body()).toEqual(null); 417 | return res; 418 | }); 419 | 420 | const xhr = new MockXMLHttpRequest(); 421 | xhr.onload = successOnEvent(done); 422 | xhr.onerror = failOnEvent(done); 423 | xhr.open('get', '/'); 424 | xhr.send(); 425 | }); 426 | 427 | it('should time out when .timeout > 0 and no response is resloved within the time', done => { 428 | let start: number, end: number; 429 | 430 | MockXMLHttpRequest.addHandler((req, res) => new Promise(() => {})); 431 | 432 | const xhr = new MockXMLHttpRequest(); 433 | xhr.timeout = 100; 434 | xhr.ontimeout = () => { 435 | end = Date.now(); 436 | expect(end - start).toBeGreaterThanOrEqual(100); 437 | expect(xhr.readyState).toEqual(4); 438 | done(); 439 | }; 440 | xhr.onerror = failOnEvent(done); 441 | start = Date.now(); 442 | xhr.open('get', '/'); 443 | xhr.send(); 444 | }); 445 | 446 | it('should not time out when .timeout > 0 and the request was aborted', done => { 447 | MockXMLHttpRequest.addHandler((req, res) => new Promise(() => {})); 448 | const xhr = new MockXMLHttpRequest(); 449 | xhr.timeout = 100; 450 | xhr.ontimeout = failOnEvent(done); 451 | xhr.onabort = successOnEvent(done); 452 | xhr.onerror = failOnEvent(done); 453 | xhr.open('get', '/'); 454 | xhr.send(); 455 | xhr.abort(); 456 | }); 457 | 458 | it('should not time out when .timeout > 0 and the request errored', done => { 459 | MockXMLHttpRequest.addHandler((req, res) => 460 | Promise.reject(new Error('test!')) 461 | ); 462 | const xhr = new MockXMLHttpRequest(); 463 | xhr.timeout = 100; 464 | xhr.ontimeout = failOnEvent(done); 465 | xhr.onerror = successOnEvent(done); 466 | xhr.open('get', '/'); 467 | xhr.send(); 468 | }); 469 | 470 | it('should set the request Content-Type header when the request Content-Type header has not been set and a body has been provided', done => { 471 | MockXMLHttpRequest.addHandler((req, res) => { 472 | expect(req.header('content-type')).toEqual('text/plain; charset=UTF-8'); 473 | return res; 474 | }); 475 | 476 | const xhr = new MockXMLHttpRequest(); 477 | xhr.onload = successOnEvent(done); 478 | xhr.onerror = failOnEvent(done); 479 | xhr.open('post', '/'); 480 | xhr.send('hello world!'); 481 | }); 482 | 483 | it('should not set the request Content-Type header when the request Content-Type header has been set and a body has been provided', done => { 484 | MockXMLHttpRequest.addHandler((req, res) => { 485 | expect(req.header('content-type')).toEqual('foo/bar'); 486 | return res; 487 | }); 488 | 489 | const xhr = new MockXMLHttpRequest(); 490 | xhr.onload = successOnEvent(done); 491 | xhr.onerror = failOnEvent(done); 492 | xhr.open('post', '/'); 493 | xhr.setRequestHeader('content-type', 'foo/bar'); 494 | xhr.send('hello world!'); 495 | }); 496 | 497 | it('should call the error callback when there is an error', done => { 498 | expect.assertions(2); 499 | MockXMLHttpRequest.addHandler((req, res) => 500 | Promise.reject(new Error('test!')) 501 | ); 502 | MockXMLHttpRequest.errorCallback = ({req, err}) => { 503 | expect(req).toBeInstanceOf(MockRequest); 504 | expect(err).toBeInstanceOf(Error); 505 | }; 506 | 507 | const xhr = new MockXMLHttpRequest(); 508 | xhr.onload = failOnEvent(done); 509 | xhr.onerror = successOnEvent(done); 510 | xhr.open('get', '/'); 511 | xhr.send(); 512 | }); 513 | 514 | it('should be able to send another request after the previous request errored', done => { 515 | MockXMLHttpRequest.addHandler((req, res) => 516 | Promise.reject(new Error('test!')) 517 | ); 518 | 519 | const xhr = new MockXMLHttpRequest(); 520 | xhr.timeout = 100; 521 | xhr.ontimeout = failOnEvent(done); 522 | xhr.onerror = () => { 523 | try { 524 | xhr.open('get', '/'); 525 | xhr.send(); 526 | xhr.abort(); 527 | done(); 528 | } catch (err) { 529 | done.fail(err); 530 | } 531 | }; 532 | xhr.open('get', '/'); 533 | xhr.send(); 534 | }); 535 | 536 | it('should error when no handlers are registered', done => { 537 | expect.assertions(2); 538 | 539 | MockXMLHttpRequest.errorCallback = ({req, err}) => { 540 | expect(req).toBeInstanceOf(MockRequest); 541 | expect(err).toBeInstanceOf(MockError); 542 | }; 543 | 544 | const xhr = new MockXMLHttpRequest(); 545 | xhr.onload = failOnEvent(done); 546 | xhr.onerror = successOnEvent(done); 547 | xhr.open('get', '/'); 548 | xhr.send(); 549 | }); 550 | }); 551 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/MockXMLHttpRequest.ts: -------------------------------------------------------------------------------- 1 | import {MockURL, parseURL, formatURL} from './MockURL'; 2 | import {MockFunction, MockHeaders, ErrorCallbackEvent} from './types'; 3 | import MockRequest from './MockRequest'; 4 | import MockResponse from './MockResponse'; 5 | import MockEvent from './MockEvent'; 6 | import MockProgressEvent from './MockProgressEvent'; 7 | import MockXMLHttpRequestUpload from './MockXMLHttpRequestUpload'; 8 | import MockXMLHttpRequestEventTarget from './MockXMLHttpRequestEventTarget'; 9 | import {sync as handleSync, async as handleAsync} from './handle'; 10 | import {formatError} from './formatError'; 11 | import {MockError} from './MockError'; 12 | 13 | const notImplementedError = new MockError( 14 | "This feature hasn't been implmented yet. Please submit an Issue or Pull Request on Github." 15 | ); 16 | 17 | const InvalidStateError = new MockError('InvalidStateError: DOM Exception 11'); 18 | 19 | // implemented according to https://xhr.spec.whatwg.org/ 20 | 21 | const FORBIDDEN_METHODS = ['CONNECT', 'TRACE', 'TRACK']; 22 | 23 | export enum ReadyState { 24 | UNSENT = 0, 25 | OPENED = 1, 26 | HEADERS_RECEIVED = 2, 27 | LOADING = 3, 28 | DONE = 4 29 | } 30 | 31 | function calculateProgress(req: MockRequest | MockResponse) { 32 | const header = req.header('content-length'); 33 | const body = req.body(); 34 | 35 | let lengthComputable = false; 36 | let total = 0; 37 | 38 | if (header) { 39 | const contentLength = parseInt(header, 10); 40 | if (contentLength !== NaN) { 41 | lengthComputable = true; 42 | total = contentLength; 43 | } 44 | } 45 | 46 | return { 47 | lengthComputable, 48 | loaded: (body && body.length) || 0, //FIXME: Measure bytes not (unicode) chars 49 | total 50 | }; 51 | } 52 | 53 | // @ts-ignore: https://github.com/jameslnewell/xhr-mock/issues/45 54 | export default class MockXMLHttpRequest extends MockXMLHttpRequestEventTarget 55 | implements XMLHttpRequest { 56 | static readonly UNSENT = ReadyState.UNSENT; 57 | static readonly OPENED = ReadyState.OPENED; 58 | static readonly HEADERS_RECEIVED = ReadyState.HEADERS_RECEIVED; 59 | static readonly LOADING = ReadyState.LOADING; 60 | static readonly DONE = ReadyState.DONE; 61 | 62 | readonly UNSENT = ReadyState.UNSENT; 63 | readonly OPENED = ReadyState.OPENED; 64 | readonly HEADERS_RECEIVED = ReadyState.HEADERS_RECEIVED; 65 | readonly LOADING = ReadyState.LOADING; 66 | readonly DONE = ReadyState.DONE; 67 | 68 | onreadystatechange: (this: XMLHttpRequest, ev: Event) => any; 69 | 70 | //some libraries (like Mixpanel) use the presence of this field to check if XHR is properly supported 71 | // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials 72 | withCredentials: boolean = false; 73 | 74 | static handlers: MockFunction[] = []; 75 | static errorCallback: (event: ErrorCallbackEvent) => void = ({req, err}) => { 76 | if (err instanceof MockError) { 77 | console.error(formatError(err.message, req)); 78 | } else { 79 | console.error( 80 | formatError('A handler returned an error for the request.', req, err) 81 | ); 82 | } 83 | }; 84 | 85 | /** 86 | * Add a mock handler 87 | */ 88 | static addHandler(fn: MockFunction): void { 89 | this.handlers.push(fn); 90 | } 91 | 92 | /** 93 | * Remove a mock handler 94 | */ 95 | static removeHandler(fn: MockFunction): void { 96 | throw notImplementedError; 97 | } 98 | 99 | /** 100 | * Remove all request handlers 101 | */ 102 | static removeAllHandlers(): void { 103 | this.handlers = []; 104 | } 105 | 106 | private req: MockRequest = new MockRequest(); 107 | private res: MockResponse = new MockResponse(); 108 | 109 | responseType: XMLHttpRequestResponseType = ''; 110 | responseURL: string = ''; 111 | private _timeout: number = 0; 112 | // @ts-ignore: https://github.com/jameslnewell/xhr-mock/issues/45 113 | upload: XMLHttpRequestUpload = new MockXMLHttpRequestUpload(); 114 | readyState: ReadyState = MockXMLHttpRequest.UNSENT; 115 | 116 | // flags 117 | private isSynchronous: boolean = false; 118 | private isSending: boolean = false; 119 | private isUploadComplete: boolean = false; 120 | private isAborted: boolean = false; 121 | private isTimedOut: boolean = false; 122 | 123 | // @ts-ignore: wants a NodeJS.Timer because of @types/node 124 | private _timeoutTimer: number; 125 | 126 | get timeout(): number { 127 | return this._timeout; 128 | } 129 | 130 | set timeout(timeout: number) { 131 | if (timeout !== 0 && this.isSynchronous) { 132 | throw new MockError( 133 | 'Timeouts cannot be set for synchronous requests made from a document.' 134 | ); 135 | } 136 | this._timeout = timeout; 137 | } 138 | 139 | // https://xhr.spec.whatwg.org/#the-response-attribute 140 | get response(): any { 141 | if (this.responseType === '' || this.responseType === 'text') { 142 | if (this.readyState !== this.LOADING && this.readyState !== this.DONE) { 143 | return ''; 144 | } 145 | return this.responseText; 146 | } 147 | 148 | if (this.readyState !== this.DONE) { 149 | return null; 150 | } 151 | 152 | const body = this.res.body(); 153 | if (!body) { 154 | return null; 155 | } 156 | 157 | if (this.responseType === 'json' && typeof body === 'string') { 158 | try { 159 | return JSON.parse(this.responseText); 160 | } catch (error) { 161 | return null; 162 | } 163 | } 164 | 165 | if (this.responseType === 'blob' && typeof body === 'string') { 166 | try { 167 | throw notImplementedError; 168 | } catch (error) { 169 | return null; 170 | } 171 | } 172 | 173 | if (this.responseType === 'arraybuffer' && typeof body === 'string') { 174 | try { 175 | throw notImplementedError; 176 | } catch (error) { 177 | return null; 178 | } 179 | } 180 | 181 | if (this.responseType === 'document' && typeof body === 'string') { 182 | try { 183 | throw notImplementedError; 184 | } catch (error) { 185 | return null; 186 | } 187 | } 188 | 189 | // rely on the mock to do the right thing with an arraybuffer, blob or document 190 | return body; 191 | } 192 | 193 | get responseText(): string { 194 | return this.res.body() || ''; 195 | } 196 | 197 | get responseXML(): Document | null { 198 | if (this.responseType === '' || this.responseType === 'document') { 199 | if ( 200 | this.readyState === this.DONE && 201 | typeof Document !== 'undefined' && 202 | this.res.body() instanceof Document 203 | ) { 204 | return this.res.body(); 205 | } 206 | return null; 207 | } 208 | 209 | throw InvalidStateError; 210 | } 211 | 212 | get status(): number { 213 | return this.res.status(); 214 | } 215 | 216 | get statusText(): string { 217 | return this.res.reason(); 218 | } 219 | 220 | getAllResponseHeaders(): string { 221 | // I'm pretty sure this fn can return null, but TS types say no 222 | // if (this.readyState < MockXMLHttpRequest.HEADERS_RECEIVED) { 223 | // return null; 224 | // } 225 | const headers = this.res.headers(); 226 | const result = Object.keys(headers) 227 | .map(name => `${name}: ${headers[name]}\r\n`) 228 | .join(''); 229 | 230 | return result; 231 | } 232 | 233 | getResponseHeader(name: string): null | string { 234 | if (this.readyState < MockXMLHttpRequest.HEADERS_RECEIVED) { 235 | return null; 236 | } 237 | 238 | return this.res.header(name); 239 | } 240 | 241 | setRequestHeader(name: string, value: string): void { 242 | if (this.readyState < MockXMLHttpRequest.OPENED) { 243 | throw new MockError('xhr must be OPENED.'); 244 | } 245 | 246 | this.req.header(name, value); 247 | } 248 | 249 | overrideMimeType(mime: string): void { 250 | throw notImplementedError; 251 | } 252 | 253 | open( 254 | method: string, 255 | url: string, 256 | async: boolean = true, 257 | username: string | null = null, 258 | password: string | null = null 259 | ): void { 260 | // if method is not a method, then throw a "SyntaxError" DOMException 261 | // if method is a forbidden method, then throw a "SecurityError" DOMException 262 | if (FORBIDDEN_METHODS.indexOf(method) !== -1) { 263 | throw new MockError(`Method ${method} is forbidden.`); 264 | } 265 | 266 | // normalize method 267 | method = method.toUpperCase(); 268 | 269 | // let parsedURL be the result of parsing url with settingsObject’s API base URL and settingsObject’s API URL character encoding 270 | // if parsedURL is failure, then throw a "SyntaxError" DOMException 271 | const fullURL = parseURL(url); 272 | 273 | // if the async argument is omitted, set async to true, and set username and password to null. 274 | 275 | // if parsedURL’s host is non-null, run these substeps: 276 | // if the username argument is not null, set the username given parsedURL and username 277 | // if the password argument is not null, set the password given parsedURL and password 278 | fullURL.username = username || ''; 279 | fullURL.password = (username && password) || ''; 280 | 281 | // if async is false, current global object is a Window object, and the timeout attribute value 282 | // is not zero or the responseType attribute value is not the empty string, then throw an "InvalidAccessError" DOMException. 283 | if (!async && (this._timeout !== 0 || this.responseType !== '')) { 284 | throw new MockError('InvalidAccessError'); 285 | } 286 | 287 | // terminate the ongoing fetch operated by the XMLHttpRequest object 288 | if (this.isSending) { 289 | throw new MockError('Unable to terminate the previous request'); 290 | } 291 | 292 | // set variables associated with the object as follows: 293 | // - unset the send() flag and upload listener flag 294 | // - set the synchronous flag, if async is false, and unset the synchronous flag otherwise 295 | // - set request method to method 296 | // - set request URL to parsedURL 297 | // - empty author request headers 298 | this.isSending = false; 299 | this.isSynchronous = !async; 300 | this.req 301 | .method(method) 302 | .headers({}) 303 | .url(formatURL(fullURL)); 304 | this.applyNetworkError(); 305 | 306 | // if the state is not opened, run these substeps: 307 | if (this.readyState !== this.OPENED) { 308 | // set state to opened 309 | this.readyState = MockXMLHttpRequest.OPENED; 310 | 311 | // fire an event named readystatechange 312 | this.dispatchEvent(new MockEvent('readystatechange')); 313 | } 314 | } 315 | 316 | private sendSync() { 317 | // let response be the result of fetching req 318 | let res; 319 | try { 320 | res = handleSync(MockXMLHttpRequest.handlers, this.req, this.res); 321 | 322 | // if the timeout attribute value is not zero, then set the timed out flag and terminate fetching if it has not returned within the amount of milliseconds from the timeout. 323 | // TODO: check if timeout was elapsed 324 | 325 | //if response’s body is null, then run handle response end-of-body and return 326 | // let reader be the result of getting a reader from response’s body’s stream 327 | // let promise be the result of reading all bytes from response’s body’s stream with reader 328 | // wait for promise to be fulfilled or rejected 329 | // if promise is fulfilled with bytes, then append bytes to received bytes 330 | // run handle response end-of-body for response 331 | this.handleResponseBody(res); 332 | } catch (error) { 333 | MockXMLHttpRequest.errorCallback({req: this.req, err: error}); 334 | this.handleError(error); 335 | } 336 | } 337 | 338 | private async sendAsync() { 339 | const req = this.req; 340 | 341 | // fire a progress event named loadstart with 0 and 0 342 | const progress = calculateProgress(this.res); 343 | this.dispatchEvent( 344 | new MockProgressEvent('loadstart', { 345 | ...progress, 346 | loaded: 0 347 | }) 348 | ); 349 | 350 | // if the upload complete flag is unset and upload listener flag is set, then fire a progress 351 | // event named loadstart on the XMLHttpRequestUpload object with 0 and req’s body’s total bytes. 352 | if (!this.isUploadComplete) { 353 | const progress = calculateProgress(this.req); 354 | this.upload.dispatchEvent( 355 | new MockProgressEvent('loadstart', { 356 | ...progress, 357 | loaded: 0 358 | }) 359 | ); 360 | } 361 | 362 | // if state is not opened or the send() flag is unset, then return. 363 | if (this.readyState !== this.OPENED || !this.isSending) { 364 | return; 365 | } 366 | 367 | // fetch req. Handle the tasks queued on the networking task source per below 368 | // run these subsubsteps in parallel: 369 | // wait until either req’s done flag is set or 370 | // the timeout attribute value number of milliseconds has passed since these subsubsteps started 371 | // while timeout attribute value is not zero 372 | // if req’s done flag is unset, then set the timed out flag and terminate fetching 373 | 374 | if (this._timeout !== 0) { 375 | // @ts-ignore: wants a NodeJS.Timer because of @types/node 376 | this._timeoutTimer = setTimeout(() => { 377 | this.isTimedOut = true; 378 | this.handleError(); 379 | }, this._timeout); 380 | } 381 | 382 | try { 383 | const res = await handleAsync( 384 | MockXMLHttpRequest.handlers, 385 | this.req, 386 | this.res 387 | ); 388 | 389 | //we've received a response before the timeout so we don't want to timeout 390 | clearTimeout(this._timeoutTimer); 391 | 392 | if (this.isAborted || this.isTimedOut) { 393 | return; // these cases will already have been handled 394 | } 395 | 396 | this.sendRequest(req); 397 | this.receiveResponse(res); 398 | } catch (error) { 399 | //we've received an error before the timeout so we don't want to timeout 400 | clearTimeout(this._timeoutTimer); 401 | 402 | if (this.isAborted || this.isTimedOut) { 403 | return; // these cases will already have been handled 404 | } 405 | 406 | MockXMLHttpRequest.errorCallback({req: this.req, err: error}); 407 | this.handleError(error); 408 | } 409 | } 410 | 411 | private applyNetworkError() { 412 | // a network error is a response whose status is always 0, status message is always the 413 | // empty byte sequence, header list is always empty, body is always null, and 414 | // trailer is always empty 415 | this.res 416 | .status(0) 417 | .reason('') 418 | .headers({}) 419 | .body(null); 420 | } 421 | 422 | // @see https://xhr.spec.whatwg.org/#request-error-steps 423 | private reportError(event: string) { 424 | // set state to done 425 | this.readyState = this.DONE; 426 | 427 | // unset the send() flag 428 | this.isSending = false; 429 | 430 | // set response to network error 431 | this.applyNetworkError(); 432 | 433 | // if the synchronous flag is set, throw an exception exception 434 | if (this.isSynchronous) { 435 | throw new MockError( 436 | 'An error occurred whilst sending a synchronous request.' 437 | ); 438 | } 439 | 440 | // fire an event named readystatechange 441 | this.dispatchEvent(new MockEvent('readystatechange')); 442 | 443 | // if the upload complete flag is unset, follow these substeps: 444 | if (!this.isUploadComplete) { 445 | // set the upload complete flag 446 | this.isUploadComplete = true; 447 | 448 | // if upload listener flag is unset, then terminate these substeps 449 | // NOTE: not sure why this is necessary - if there's no listeners listening, then the 450 | // following events have no impact 451 | 452 | const uploadProgress = calculateProgress(this.req); 453 | 454 | // fire a progress event named event on the XMLHttpRequestUpload object with 0 and 0 455 | this.upload.dispatchEvent(new MockProgressEvent(event, uploadProgress)); 456 | 457 | // fire a progress event named loadend on the XMLHttpRequestUpload object with 0 and 0 458 | this.upload.dispatchEvent( 459 | new MockProgressEvent('loadend', uploadProgress) 460 | ); 461 | } 462 | 463 | const downloadProgress = calculateProgress(this.res); 464 | 465 | // fire a progress event named event with 0 and 0 466 | this.dispatchEvent(new MockProgressEvent(event, downloadProgress)); 467 | 468 | // fire a progress event named loadend with 0 and 0 469 | this.dispatchEvent(new MockProgressEvent('loadend', downloadProgress)); 470 | } 471 | 472 | private sendRequest(req: MockRequest) { 473 | if (this.isUploadComplete) { 474 | return; 475 | } 476 | 477 | // if not roughly 50ms have passed since these subsubsteps were last invoked, terminate these subsubsteps 478 | // TODO: 479 | 480 | // If upload listener flag is set, then fire a progress event named progress on the 481 | // XMLHttpRequestUpload object with request’s body’s transmitted bytes and request’s body’s 482 | // total bytes 483 | // const progress = getProgress(this.req); 484 | // this.upload.dispatchEvent(new MockProgressEvent('progress', { 485 | // ...progress, 486 | // loaded: % 487 | // })) 488 | // TODO: repeat this in a timeout to simulate progress events 489 | // TODO: dispatch total, length and lengthComputable values 490 | 491 | // set the upload complete flag 492 | this.isUploadComplete = true; 493 | 494 | // if upload listener flag is unset, then terminate these subsubsteps. 495 | // NOTE: it doesn't really matter if we emit these events and noone is listening 496 | 497 | // let transmitted be request’s body’s transmitted bytes 498 | // let length be request’s body’s total bytes 499 | const progress = calculateProgress(this.req); 500 | 501 | // fire a progress event named progress on the XMLHttpRequestUpload object with transmitted and length 502 | this.upload.dispatchEvent(new MockProgressEvent('progress', progress)); 503 | 504 | // fire a progress event named load on the XMLHttpRequestUpload object with transmitted and length 505 | this.upload.dispatchEvent(new MockProgressEvent('load', progress)); 506 | 507 | // fire a progress event named loadend on the XMLHttpRequestUpload object with transmitted and length 508 | this.upload.dispatchEvent(new MockProgressEvent('loadend', progress)); 509 | } 510 | 511 | private receiveResponse(res: MockResponse) { 512 | // set state to headers received 513 | this.readyState = this.HEADERS_RECEIVED; 514 | 515 | // fire an event named readystatechange 516 | this.dispatchEvent(new MockEvent('readystatechange')); 517 | 518 | // if state is not headers received, then return 519 | // NOTE: is that really necessary, we've just change the state a second ago 520 | 521 | // if response’s body is null, then run handle response end-of-body and return 522 | if (res.body() === null) { 523 | this.handleResponseBody(res); 524 | return; 525 | } 526 | 527 | // let reader be the result of getting a reader from response’s body’s stream 528 | // let read be the result of reading a chunk from response’s body’s stream with reader 529 | // When read is fulfilled with an object whose done property is false and whose value property 530 | // is a Uint8Array object, run these subsubsubsteps and then run the above subsubstep again: 531 | // TODO: 532 | 533 | // append the value property to received bytes 534 | 535 | // if not roughly 50ms have passed since these subsubsubsteps were last invoked, then terminate 536 | // these subsubsubsteps 537 | // TODO: 538 | 539 | // if state is headers received, then set state to loading 540 | // NOTE: why wouldn't it be headers received? 541 | this.readyState = this.LOADING; 542 | 543 | // fire an event named readystatechange 544 | this.dispatchEvent(new MockEvent('readystatechange')); 545 | 546 | // fire a progress event named progress with response’s body’s transmitted bytes and response’s 547 | // body’s total bytes 548 | // TODO: repeat to simulate progress 549 | // const progress = calculateProgress(res); 550 | // this.dispatchEvent(new MockProgressEvent('progress', { 551 | // ...progress, 552 | // loaded: % 553 | // })); 554 | 555 | // when read is fulfilled with an object whose done property is true, run handle response 556 | // end-of-body for response 557 | // when read is rejected with an exception, run handle errors for response 558 | // NOTE: we don't handle this error case 559 | this.handleResponseBody(res); 560 | } 561 | 562 | // @see https://xhr.spec.whatwg.org/#handle-errors 563 | private handleError(error?: Error) { 564 | // if the send() flag is unset, return 565 | if (!this.isSending) { 566 | return; 567 | } 568 | 569 | // if the timed out flag is set, then run the request error steps for event timeout and exception TimeoutError 570 | if (this.isTimedOut) { 571 | this.reportError('timeout'); 572 | return; 573 | } 574 | 575 | // otherwise, if response’s body’s stream is errored, then: 576 | // NOTE: we're not handling this event 577 | // if () { 578 | 579 | // // set state to done 580 | // this.readyState = this.DONE; 581 | 582 | // // unset the send() flag 583 | // this.isSending = false; 584 | 585 | // // set response to a network error 586 | // this.applyNetworkError(); 587 | 588 | // return; 589 | // } 590 | 591 | // otherwise, if response’s aborted flag is set, then run the request error steps for event abort and exception AbortError 592 | if (this.isAborted) { 593 | this.reportError('abort'); 594 | return; 595 | } 596 | 597 | // if response is a network error, run the request error steps for event error and exception NetworkError 598 | // NOTE: we assume all other calls are network errors 599 | this.reportError('error'); 600 | } 601 | 602 | // @see https://xhr.spec.whatwg.org/#handle-response-end-of-body 603 | private handleResponseBody(res: MockResponse) { 604 | this.res = res; 605 | 606 | // let transmitted be response’s body’s transmitted bytes 607 | // let length be response’s body’s total bytes. 608 | const progress = calculateProgress(res); 609 | 610 | // if the synchronous flag is unset, update response’s body using response 611 | if (!this.isSynchronous) { 612 | // fire a progress event named progress with transmitted and length 613 | this.dispatchEvent(new MockProgressEvent('progress', progress)); 614 | } 615 | 616 | // set state to done 617 | this.readyState = this.DONE; 618 | 619 | // unset the send() flag 620 | this.isSending = false; 621 | 622 | // fire an event named readystatechange 623 | this.dispatchEvent(new MockEvent('readystatechange')); 624 | 625 | // fire a progress event named load with transmitted and length 626 | this.dispatchEvent(new MockProgressEvent('load', progress)); 627 | 628 | // fire a progress event named loadend with transmitted and length 629 | this.dispatchEvent(new MockProgressEvent('loadend', progress)); 630 | } 631 | 632 | // https://xhr.spec.whatwg.org/#event-xhr-loadstart 633 | send(): void; 634 | send(body?: any): void; 635 | send(body?: any): void { 636 | // if state is not opened, throw an InvalidStateError exception 637 | if (this.readyState !== MockXMLHttpRequest.OPENED) { 638 | throw new MockError( 639 | 'Please call MockXMLHttpRequest.open() before MockXMLHttpRequest.send().' 640 | ); 641 | } 642 | 643 | // if the send() flag is set, throw an InvalidStateError exception 644 | if (this.isSending) { 645 | throw new MockError('MockXMLHttpRequest.send() has already been called.'); 646 | } 647 | 648 | // if the request method is GET or HEAD, set body to null 649 | if (this.req.method() === 'GET' || this.req.method() === 'HEAD') { 650 | body = null; 651 | } 652 | 653 | // if body is null, go to the next step otherwise, let encoding and mimeType be null, and then follow these rules, switching on body 654 | let encoding; 655 | let mimeType; 656 | if (body !== null && body !== undefined) { 657 | if ( 658 | typeof Document !== 'undefined' && 659 | typeof XMLDocument !== 'undefined' && 660 | body instanceof Document 661 | ) { 662 | // Set encoding to `UTF-8`. 663 | // Set mimeType to `text/html` if body is an HTML document, and to `application/xml` otherwise. Then append `;charset=UTF-8` to mimeType. 664 | // Set request body to body, serialized, converted to Unicode, and utf-8 encoded. 665 | encoding = 'UTF-8'; 666 | mimeType = 667 | body instanceof XMLDocument ? 'application/xml' : 'text/html'; 668 | } else { 669 | // If body is a string, set encoding to `UTF-8`. 670 | // Set request body and mimeType to the result of extracting body. 671 | // https://fetch.spec.whatwg.org/#concept-bodyinit-extract 672 | 673 | if (typeof Blob !== 'undefined' && body instanceof Blob) { 674 | mimeType = body.type; 675 | } else if ( 676 | typeof FormData !== 'undefined' && 677 | body instanceof FormData 678 | ) { 679 | mimeType = 'multipart/form-data; boundary=----XHRMockFormBoundary'; 680 | } else if ( 681 | typeof URLSearchParams !== 'undefined' && 682 | body instanceof URLSearchParams 683 | ) { 684 | encoding = 'UTF-8'; 685 | mimeType = 'application/x-www-form-urlencoded'; 686 | } else if (typeof body === 'string') { 687 | encoding = 'UTF-8'; 688 | mimeType = 'text/plain'; 689 | } else { 690 | throw notImplementedError; 691 | } 692 | } 693 | 694 | // if mimeType is non-null and author request headers does not contain `Content-Type`, then append `Content-Type`/mimeType to author request headers. 695 | // otherwise, if the header whose name is a byte-case-insensitive match for `Content-Type` in author request headers has a value that is a valid MIME type, 696 | // which has a `charset` parameter whose value is not a byte-case-insensitive match for encoding, and encoding is not null, then set all the `charset` parameters 697 | // whose value is not a byte-case-insensitive match for encoding of that header’s value to encoding. 698 | // chrome seems to forget the second case ^^^ 699 | const contentType = this.req.header('content-type'); 700 | if (!contentType) { 701 | this.req.header( 702 | 'content-type', 703 | encoding ? `${mimeType}; charset=${encoding}` : mimeType 704 | ); 705 | } 706 | 707 | this.req.body(body); 708 | } 709 | 710 | // if one or more event listeners are registered on the associated XMLHttpRequestUpload object, then set upload listener flag 711 | // Note: not really necessary since dispatching an event to no listeners doesn't hurt anybody 712 | 713 | //TODO: check CORs 714 | 715 | // unset the upload complete flag 716 | this.isUploadComplete = false; 717 | 718 | // unset the timed out flag 719 | this.isTimedOut = false; 720 | 721 | // if req’s body is null, set the upload complete flag 722 | if (body === null || body === undefined) { 723 | this.isUploadComplete = true; 724 | } 725 | 726 | // set the send() flag 727 | this.isSending = true; 728 | 729 | if (this.isSynchronous) { 730 | this.sendSync(); 731 | } else { 732 | this.sendAsync(); 733 | } 734 | } 735 | 736 | abort(): void { 737 | //we've cancelling the response before the timeout period so we don't want to timeout 738 | clearTimeout(this._timeoutTimer); 739 | 740 | // terminate the ongoing fetch with the aborted flag set 741 | this.isAborted = true; 742 | 743 | // if state is either opened with the send() flag set, headers received, or loading, 744 | // run the request error steps for event 745 | if ( 746 | this.readyState === this.OPENED || 747 | this.readyState === this.HEADERS_RECEIVED || 748 | this.readyState === this.LOADING 749 | ) { 750 | this.reportError('abort'); 751 | } 752 | 753 | // if state is done, then set state to unsent and response to a network error 754 | if (this.readyState === this.DONE) { 755 | this.readyState = this.UNSENT; 756 | this.applyNetworkError(); 757 | return; 758 | } 759 | } 760 | 761 | msCachingEnabled() { 762 | return false; 763 | } 764 | } 765 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/MockXMLHttpRequestEventTarget.test.ts: -------------------------------------------------------------------------------- 1 | import MockEvent from './MockEvent'; 2 | import MockXMLHttpRequestEventTarget from './MockXMLHttpRequestEventTarget'; 3 | 4 | describe('MockXMLHttpRequestEventTarget', () => { 5 | it('should call onabort', () => { 6 | const target = new MockXMLHttpRequestEventTarget(); 7 | 8 | const listener = jest.fn(); 9 | target.onabort = listener; 10 | 11 | target.dispatchEvent(new MockEvent('abort')); 12 | 13 | expect(listener.mock.calls).toHaveLength(1); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/MockXMLHttpRequestEventTarget.ts: -------------------------------------------------------------------------------- 1 | import MockEvent from './MockEvent'; 2 | import MockProgressEvent from './MockProgressEvent'; 3 | import MockEventTarget from './MockEventTarget'; 4 | 5 | // @ts-ignore: https://github.com/jameslnewell/xhr-mock/issues/45 6 | export default class MockXMLHttpRequestEventTarget extends MockEventTarget 7 | implements XMLHttpRequestEventTarget { 8 | onabort: (this: XMLHttpRequestEventTarget, ev: ProgressEvent) => any; 9 | // @ts-ignore: https://github.com/jameslnewell/xhr-mock/issues/45 10 | onerror: (this: XMLHttpRequestEventTarget, ev: ProgressEvent) => any; 11 | onload: (this: XMLHttpRequestEventTarget, ev: ProgressEvent) => any; 12 | onloadend: (this: XMLHttpRequestEventTarget, ev: ProgressEvent) => any; 13 | onloadstart: (this: XMLHttpRequestEventTarget, ev: ProgressEvent) => any; 14 | onprogress: (this: XMLHttpRequestEventTarget, ev: ProgressEvent) => any; 15 | ontimeout: (this: XMLHttpRequestEventTarget, ev: ProgressEvent) => any; 16 | } 17 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/MockXMLHttpRequestUpload.ts: -------------------------------------------------------------------------------- 1 | import MockXMLHttpRequestEventTarget from './MockXMLHttpRequestEventTarget'; 2 | 3 | // @ts-ignore: https://github.com/jameslnewell/xhr-mock/issues/45 4 | export default class MockXMLHttpRequestUpload extends MockXMLHttpRequestEventTarget 5 | implements XMLHttpRequestUpload {} 6 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/XHRMock.test.ts: -------------------------------------------------------------------------------- 1 | import window = require('global'); 2 | import Mock from '.'; 3 | import MockXMLHttpRequest from './MockXMLHttpRequest'; 4 | 5 | describe('xhr-mock', () => { 6 | it('should replace the original XHR on setup and restore the original XHR on teardown', () => { 7 | const originalXHR = window.XMLHttpRequest; 8 | expect(window.XMLHttpRequest).toBe(originalXHR); 9 | 10 | Mock.setup(); 11 | expect(window.XMLHttpRequest).not.toBe(originalXHR); 12 | 13 | Mock.teardown(); 14 | expect(window.XMLHttpRequest).toBe(originalXHR); 15 | }); 16 | 17 | it('should remove all handlers on setup, on reset and on teardown', () => { 18 | Mock.get('http://www.google.com/', {}); 19 | Mock.setup(); 20 | expect(MockXMLHttpRequest.handlers).toHaveLength(0); 21 | 22 | Mock.get('http://www.google.com/', {}); 23 | Mock.reset(); 24 | expect(MockXMLHttpRequest.handlers).toHaveLength(0); 25 | 26 | Mock.get('http://www.google.com/', {}); 27 | Mock.teardown(); 28 | expect(MockXMLHttpRequest.handlers).toHaveLength(0); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/XHRMock.ts: -------------------------------------------------------------------------------- 1 | import window = require('global'); 2 | import {Mock, MockFunction, ErrorCallbackEvent} from './types'; 3 | import createMockFunction from './createMockFunction'; 4 | import MockXMLHttpRequest from './MockXMLHttpRequest'; 5 | 6 | const RealXMLHttpRequest = window.XMLHttpRequest; 7 | 8 | export class XHRMock { 9 | RealXMLHttpRequest: {new (): XMLHttpRequest} = RealXMLHttpRequest; 10 | 11 | setup(): XHRMock { 12 | // @ts-ignore: https://github.com/jameslnewell/xhr-mock/issues/45 13 | window.XMLHttpRequest = MockXMLHttpRequest; 14 | this.reset(); 15 | return this; 16 | } 17 | 18 | teardown(): XHRMock { 19 | this.reset(); 20 | window.XMLHttpRequest = RealXMLHttpRequest; 21 | return this; 22 | } 23 | 24 | reset(): XHRMock { 25 | MockXMLHttpRequest.removeAllHandlers(); 26 | return this; 27 | } 28 | 29 | error(callback: (event: ErrorCallbackEvent) => void): XHRMock { 30 | MockXMLHttpRequest.errorCallback = callback; 31 | return this; 32 | } 33 | 34 | mock(fn: MockFunction): XHRMock; 35 | mock(method: string, url: string | RegExp, mock: Mock): XHRMock; 36 | mock( 37 | fnOrMethod: string | MockFunction, 38 | url?: string | RegExp, 39 | mock?: Mock 40 | ): XHRMock { 41 | console.warn( 42 | 'xhr-mock: XHRMock.mock() has been deprecated. Use XHRMock.use() instead.' 43 | ); 44 | if ( 45 | typeof fnOrMethod === 'string' && 46 | (typeof url === 'string' || url instanceof RegExp) && 47 | mock !== undefined 48 | ) { 49 | return this.use(fnOrMethod, url, mock); 50 | } else if (typeof fnOrMethod === 'function') { 51 | return this.use(fnOrMethod); 52 | } else { 53 | throw new Error('xhr-mock: Invalid handler.'); 54 | } 55 | } 56 | 57 | use(fn: MockFunction): XHRMock; 58 | use(method: string, url: string | RegExp, mock: Mock): XHRMock; 59 | use( 60 | fnOrMethod: string | MockFunction, 61 | url?: string | RegExp, 62 | mock?: Mock 63 | ): XHRMock { 64 | let fn: MockFunction; 65 | if ( 66 | typeof fnOrMethod === 'string' && 67 | (typeof url === 'string' || url instanceof RegExp) && 68 | mock !== undefined 69 | ) { 70 | fn = createMockFunction(fnOrMethod, url, mock); 71 | } else if (typeof fnOrMethod === 'function') { 72 | fn = fnOrMethod; 73 | } else { 74 | throw new Error('xhr-mock: Invalid handler.'); 75 | } 76 | MockXMLHttpRequest.addHandler(fn); 77 | return this; 78 | } 79 | 80 | get(url: string | RegExp, mock: Mock): XHRMock { 81 | return this.use('GET', url, mock); 82 | } 83 | 84 | post(url: string | RegExp, mock: Mock): XHRMock { 85 | return this.use('POST', url, mock); 86 | } 87 | 88 | put(url: string | RegExp, mock: Mock): XHRMock { 89 | return this.use('PUT', url, mock); 90 | } 91 | 92 | patch(url: string | RegExp, mock: Mock): XHRMock { 93 | return this.use('PATCH', url, mock); 94 | } 95 | 96 | delete(url: string | RegExp, mock: Mock): XHRMock { 97 | return this.use('DELETE', url, mock); 98 | } 99 | } 100 | 101 | // I'm only using a class so I can make use make use of TS' method overrides 102 | export default new XHRMock(); 103 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/createMockFunction.test.ts: -------------------------------------------------------------------------------- 1 | import createMockFunction from './createMockFunction'; 2 | 3 | describe('createMockFunction()', () => { 4 | //TODO: add tests 5 | it.skip('should test stuff'); 6 | }); 7 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/createMockFunction.ts: -------------------------------------------------------------------------------- 1 | import {Mock, MockObject, MockFunction} from './types'; 2 | import {formatURL} from './MockURL'; 3 | import MockRequest from './MockRequest'; 4 | import MockResponse from './MockResponse'; 5 | import {createResponseFromObject} from './createResponseFromObject'; 6 | 7 | export default function( 8 | method: string, 9 | url: string | RegExp, 10 | mock: Mock 11 | ): MockFunction { 12 | const matches = (req: MockRequest) => { 13 | const requestMethod = req.method(); 14 | const requestURL = req.url().toString(); 15 | 16 | if (requestMethod.toUpperCase() !== method.toUpperCase()) { 17 | return false; 18 | } 19 | 20 | if (url instanceof RegExp) { 21 | url.lastIndex = 0; //reset state of global regexp 22 | return url.test(requestURL); 23 | } 24 | 25 | return requestURL === url; //TODO: should we use .startsWith()??? 26 | }; 27 | 28 | return (req, res) => { 29 | if (matches(req)) { 30 | if (typeof mock === 'object') { 31 | return createResponseFromObject(mock); 32 | } else { 33 | return mock(req, res); 34 | } 35 | } 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/createResponseFromObject.ts: -------------------------------------------------------------------------------- 1 | import {MockObject} from './types'; 2 | import MockResponse from './MockResponse'; 3 | 4 | export function createResponseFromObject(object: MockObject): MockResponse { 5 | const {status, reason, headers, body} = object; 6 | const response = new MockResponse(); 7 | 8 | if (status) { 9 | response.status(status); 10 | } 11 | 12 | if (reason) { 13 | response.reason(reason); 14 | } 15 | 16 | if (headers) { 17 | response.headers(headers); 18 | } 19 | 20 | if (body) { 21 | response.body(body); 22 | } 23 | 24 | return response; 25 | } 26 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/formatError.test.ts: -------------------------------------------------------------------------------- 1 | import {formatError} from './formatError'; 2 | import MockRequest from './MockRequest'; 3 | 4 | describe('formatError()', () => { 5 | it('should contain the request string', () => { 6 | const req = new MockRequest() 7 | .method('get') 8 | .url('/foo/bar') 9 | .header('Content-Type', 'application/json; charset=UTF-8') 10 | .body(new Blob()); 11 | const err = new Error('Uh oh!'); 12 | 13 | const formatted = formatError( 14 | 'None of the registered handlers returned a response', 15 | req, 16 | err 17 | ); 18 | 19 | expect(formatted).toContain('GET /foo/bar HTTP/1.1'); 20 | expect(formatted).toContain( 21 | 'content-type: application/json; charset=UTF-8' 22 | ); 23 | }); 24 | 25 | it('should contain the error message and stack trace', () => { 26 | const req = new MockRequest().method('get').url('/foo/bar'); 27 | const err = new Error('Uh oh!'); 28 | 29 | const formatted = formatError( 30 | 'None of the registered handlers returned a response', 31 | req, 32 | err 33 | ); 34 | 35 | expect(formatted).toContain('Uh oh'); 36 | expect(formatted).toContain('formatError.test.ts'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/formatError.ts: -------------------------------------------------------------------------------- 1 | import {ErrorCallbackEvent} from './types'; 2 | import MockRequest from './MockRequest'; 3 | 4 | function convertRequestToString(req: MockRequest): string { 5 | const headers = Object.keys(req.headers()).map( 6 | name => `${name}: ${req.header(name)}` 7 | ); 8 | const body = req.body() ? req.body() : ''; 9 | return `${req.method()} ${req.url()} HTTP/1.1 10 | ${headers ? `${headers.join('\n')}\n` : ''} 11 | ${body ? body : ''} 12 | `; 13 | } 14 | 15 | function indentSuccessiveLines(string: string, indent: number): string { 16 | return string 17 | .split('\n') 18 | .map((line, index) => Array(indent + 1).join(' ') + line) 19 | .join('\n'); 20 | } 21 | 22 | export function formatError(msg: string, req: MockRequest, err?: Error) { 23 | return `xhr-mock: ${msg} 24 | 25 | ${indentSuccessiveLines(convertRequestToString(req), 2).trim()} 26 | ${ 27 | err !== undefined 28 | ? `\n${indentSuccessiveLines( 29 | (err && err.stack) || (err && err.message) || `Error: ${err}`, 30 | 2 31 | )}` 32 | : '' 33 | } 34 | `; 35 | } 36 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/handle.ts: -------------------------------------------------------------------------------- 1 | import {MockFunction} from './types'; 2 | import MockRequest from './MockRequest'; 3 | import MockResponse from './MockResponse'; 4 | import {MockError} from './MockError'; 5 | import {isPromiseLike} from './isPromiseLike'; 6 | 7 | const NO_RESPONSE_ERROR = new MockError( 8 | 'No handler returned a response for the request.' 9 | ); 10 | 11 | export function sync( 12 | handlers: MockFunction[], 13 | request: MockRequest, 14 | response: MockResponse 15 | ): MockResponse { 16 | for (let i = 0; i < handlers.length; ++i) { 17 | const result = handlers[i](request, response); 18 | 19 | if (result) { 20 | if (isPromiseLike(result)) { 21 | throw new MockError( 22 | 'A handler returned a Promise for a synchronous request.' 23 | ); 24 | } 25 | return result; 26 | } 27 | } 28 | throw NO_RESPONSE_ERROR; 29 | } 30 | 31 | export function async( 32 | handlers: MockFunction[], 33 | request: MockRequest, 34 | response: MockResponse 35 | ): Promise { 36 | return handlers 37 | .reduce( 38 | (promise, handler) => 39 | promise.then(result => { 40 | if (!result) { 41 | return handler(request, response); 42 | } 43 | return result; 44 | }), 45 | Promise.resolve(undefined) 46 | ) 47 | .then(result => { 48 | if (!result) { 49 | throw NO_RESPONSE_ERROR; 50 | } 51 | return result; 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/index.ts: -------------------------------------------------------------------------------- 1 | import XHRMock from './XHRMock'; 2 | import MockRequest from './MockRequest'; 3 | import MockResponse from './MockResponse'; 4 | import proxy from './proxy'; 5 | import {once} from './utils/once'; 6 | import {delay} from './utils/delay'; 7 | import {sequence} from './utils/sequence'; 8 | 9 | export default XHRMock; 10 | export {MockRequest, MockResponse, proxy, once, delay, sequence}; 11 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/index.umd.ts: -------------------------------------------------------------------------------- 1 | import XHRMock from './XHRMock'; 2 | export default XHRMock; 3 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/isPromiseLike.ts: -------------------------------------------------------------------------------- 1 | import MockResponse from './MockResponse'; 2 | 3 | export function isPromiseLike( 4 | arg: any 5 | ): arg is Promise { 6 | return arg && (arg as Promise).then !== undefined; 7 | } 8 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/proxy.browser.test.ts: -------------------------------------------------------------------------------- 1 | import {__replaceRealXHR} from './XHRMock'; 2 | import MockRequest from './MockRequest'; 3 | import MockResponse from './MockResponse'; 4 | import proxy from './proxy.browser'; 5 | 6 | type RealXHRMock = { 7 | error?: Error; 8 | status: number; 9 | statusText: string; 10 | response: any; 11 | setRequestHeader: jest.Mock; 12 | getAllResponseHeaders: jest.Mock; 13 | open: jest.Mock; 14 | send: jest.Mock; 15 | onerror?: jest.Mock; 16 | onloadend?: jest.Mock; 17 | }; 18 | 19 | declare module './XHRMock' { 20 | export function __replaceRealXHR(): RealXHRMock; 21 | } 22 | 23 | jest.mock('./XHRMock', () => { 24 | let mock: RealXHRMock; 25 | return { 26 | __replaceRealXHR() { 27 | mock = { 28 | status: 200, 29 | statusText: '', 30 | response: null, 31 | setRequestHeader: jest.fn(), 32 | getAllResponseHeaders: jest.fn().mockReturnValue(''), 33 | open: jest.fn(), 34 | send: jest.fn(() => { 35 | if (mock.error && mock.onerror) { 36 | mock.onerror({error: mock.error}); 37 | } else if (mock.onloadend) { 38 | mock.onloadend(); 39 | } 40 | }) 41 | }; 42 | return mock; 43 | }, 44 | default: { 45 | RealXMLHttpRequest: jest.fn(() => mock) 46 | } 47 | }; 48 | }); 49 | 50 | describe('proxy.browser', () => { 51 | let xhr: RealXHRMock; 52 | 53 | beforeEach(() => { 54 | xhr = __replaceRealXHR(); 55 | }); 56 | 57 | it('should call open() with the method and URL', async () => { 58 | const req = new MockRequest(); 59 | const res = new MockResponse(); 60 | 61 | req.method('PUT').url('http://httpbin.org/put'); 62 | await proxy(req, res); 63 | 64 | expect(xhr.open).toBeCalledWith('PUT', 'http://httpbin.org/put'); 65 | }); 66 | 67 | it('should set the request headers', async () => { 68 | const req = new MockRequest(); 69 | const res = new MockResponse(); 70 | 71 | req.header('foo', 'bar').header('bar', 'foo'); 72 | await proxy(req, res); 73 | 74 | expect(xhr.setRequestHeader).toBeCalledWith('foo', 'bar'); 75 | }); 76 | 77 | it('should call send() with a body', async () => { 78 | const req = new MockRequest(); 79 | const res = new MockResponse(); 80 | 81 | req.body('Hello World!'); 82 | await proxy(req, res); 83 | 84 | expect(xhr.send).toBeCalledWith('Hello World!'); 85 | }); 86 | 87 | it('should call send() without a body', async () => { 88 | const req = new MockRequest(); 89 | const res = new MockResponse(); 90 | 91 | await proxy(req, res); 92 | 93 | expect(xhr.send).toBeCalledWith(null); 94 | }); 95 | 96 | it('should set the status', async () => { 97 | const req = new MockRequest(); 98 | const res = new MockResponse(); 99 | 100 | xhr.status = 201; 101 | await proxy(req, res); 102 | 103 | expect(res.status()).toEqual(201); 104 | }); 105 | 106 | it('should set the reason', async () => { 107 | const req = new MockRequest(); 108 | const res = new MockResponse(); 109 | 110 | xhr.statusText = 'Created'; 111 | await proxy(req, res); 112 | 113 | expect(res.reason()).toEqual('Created'); 114 | }); 115 | 116 | it('should set the headers', async () => { 117 | const req = new MockRequest(); 118 | const res = new MockResponse(); 119 | 120 | xhr.getAllResponseHeaders.mockReturnValue('foo: bar\r\nbar: foo\r\n'); 121 | await proxy(req, res); 122 | 123 | expect(res.headers()).toEqual( 124 | expect.objectContaining({ 125 | foo: 'bar', 126 | bar: 'foo' 127 | }) 128 | ); 129 | }); 130 | 131 | it('should set the body when .response is text', async () => { 132 | const req = new MockRequest(); 133 | const res = new MockResponse(); 134 | 135 | xhr.response = 'Hello World!'; 136 | await proxy(req, res); 137 | 138 | expect(res.body()).toEqual('Hello World!'); 139 | }); 140 | 141 | it('should set the body when response is null', async () => { 142 | const req = new MockRequest(); 143 | const res = new MockResponse(); 144 | 145 | xhr.response = null; 146 | await proxy(req, res); 147 | 148 | expect(res.body()).toEqual(null); 149 | }); 150 | 151 | it('should set the body when response is an array', async () => { 152 | const req = new MockRequest(); 153 | const res = new MockResponse(); 154 | 155 | xhr.response = []; 156 | await proxy(req, res); 157 | 158 | expect(res.body()).toEqual([]); 159 | }); 160 | 161 | it('should error', async () => { 162 | expect.assertions(1); 163 | const req = new MockRequest(); 164 | const res = new MockResponse(); 165 | 166 | xhr.error = new Error(); 167 | try { 168 | await proxy(req, res); 169 | } catch (error) { 170 | expect(error).not.toBeUndefined(); 171 | } 172 | }); 173 | }); 174 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/proxy.browser.ts: -------------------------------------------------------------------------------- 1 | import XHRMock from './XHRMock'; 2 | import MockRequest from './MockRequest'; 3 | import MockResponse from './MockResponse'; 4 | 5 | function parseHeaders(string: String): {} { 6 | const headers: {[name: string]: string} = {}; 7 | const lines = string.split('\r\n'); 8 | lines.forEach(line => { 9 | const [name, value] = line.split(':', 2); 10 | if (name && value) { 11 | headers[name] = value.replace(/^\s*/g, '').replace(/\s*$/g, ''); 12 | } 13 | }); 14 | return headers; 15 | } 16 | 17 | export default function( 18 | req: MockRequest, 19 | res: MockResponse 20 | ): Promise { 21 | return new Promise((resolve, reject) => { 22 | const xhr: XMLHttpRequest = new XHRMock.RealXMLHttpRequest(); 23 | 24 | // TODO: reject with the correct type of error 25 | xhr.onerror = event => reject(event.error); 26 | 27 | xhr.onloadend = () => { 28 | res 29 | .status(xhr.status) 30 | .reason(xhr.statusText) 31 | .headers(parseHeaders(xhr.getAllResponseHeaders())) 32 | .body(xhr.response); 33 | resolve(res); 34 | }; 35 | 36 | xhr.open(req.method(), req.url().toString()); 37 | 38 | const headers = req.headers(); 39 | Object.keys(headers).forEach(name => { 40 | const value = headers[name]; 41 | xhr.setRequestHeader(name, value); 42 | }); 43 | 44 | xhr.send(req.body()); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/proxy.test.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | import * as https from 'https'; 3 | import MockRequest from './MockRequest'; 4 | import MockResponse from './MockResponse'; 5 | import proxy from './proxy'; 6 | 7 | // declare module 'http' { 8 | // export function __reset(): void; 9 | // } 10 | 11 | // declare module 'https' { 12 | // export function __reset(): void; 13 | // } 14 | 15 | // jest.mock('http', () => { 16 | // let callback: Function; 17 | // let request: jest.Mock = jest.fn((opts, cb) => callback = cb); 18 | // return { 19 | // __reset() { 20 | // request.mockReset(); 21 | // request.mockReturnValue({ 22 | // on: jest.fn(), 23 | // end: jest.fn(() => { 24 | // callback(); 25 | // }) 26 | // }); 27 | // }, 28 | // request 29 | // }; 30 | // }); 31 | 32 | // jest.mock('https', () => { 33 | // let callback: Function; 34 | // let request: jest.Mock = jest.fn(); 35 | // return { 36 | // __reset() { 37 | // request.mockReset(); 38 | // request.mockReturnValue({ 39 | // on: jest.fn(), 40 | // end: jest.fn(() => { 41 | // callback(); 42 | // }) 43 | // }); 44 | // }, 45 | // request 46 | // }; 47 | // }); 48 | 49 | describe('proxy', () => { 50 | // beforeEach(() => { 51 | // http.__reset(); 52 | // https.__reset(); 53 | // }); 54 | 55 | it('should call http.request() with the method, URL and headers', async () => { 56 | const req = new MockRequest(); 57 | const res = new MockResponse(); 58 | 59 | req 60 | .method('PUT') 61 | .url('http://httpbin.org/put') 62 | .header('foo', 'bar') 63 | .header('bar', 'foo'); 64 | await proxy(req, res); 65 | 66 | const body = res.body() || ''; 67 | expect(JSON.parse(body)).toEqual( 68 | expect.objectContaining({ 69 | url: 'https://httpbin.org/put', 70 | headers: expect.objectContaining({ 71 | Foo: 'bar', 72 | Bar: 'foo' 73 | }) 74 | }) 75 | ); 76 | }); 77 | 78 | it('should call https.request() with the method, URL and headers', async () => { 79 | const req = new MockRequest(); 80 | const res = new MockResponse(); 81 | 82 | req 83 | .method('PUT') 84 | .url('https://httpbin.org/put') 85 | .header('foo', 'bar') 86 | .header('bar', 'foo'); 87 | await proxy(req, res); 88 | 89 | const body = res.body() || ''; 90 | expect(JSON.parse(body)).toEqual( 91 | expect.objectContaining({ 92 | url: 'https://httpbin.org/put', 93 | headers: expect.objectContaining({ 94 | Foo: 'bar', 95 | Bar: 'foo' 96 | }) 97 | }) 98 | ); 99 | }); 100 | 101 | it('should call send() with a body', async () => { 102 | const req = new MockRequest(); 103 | const res = new MockResponse(); 104 | 105 | req 106 | .method('PUT') 107 | .url('http://httpbin.org/put') 108 | .header('Content-Length', '12') 109 | .body('Hello World!'); 110 | await proxy(req, res); 111 | 112 | const body = res.body() || ''; 113 | expect(JSON.parse(body)).toEqual( 114 | expect.objectContaining({ 115 | url: 'https://httpbin.org/put', 116 | data: 'Hello World!' 117 | }) 118 | ); 119 | }); 120 | 121 | it('should call send() without a body', async () => { 122 | const req = new MockRequest(); 123 | const res = new MockResponse(); 124 | 125 | req.method('PUT').url('http://httpbin.org/put'); 126 | await proxy(req, res); 127 | 128 | const body = res.body() || ''; 129 | expect(JSON.parse(body)).toEqual( 130 | expect.objectContaining({ 131 | url: 'https://httpbin.org/put', 132 | data: '' 133 | }) 134 | ); 135 | }); 136 | 137 | it('should set the reason', async () => { 138 | const req = new MockRequest(); 139 | const res = new MockResponse(); 140 | 141 | req.method('PUT').url('http://httpbin.org/put'); 142 | await proxy(req, res); 143 | 144 | expect(res.reason()).toEqual('OK'); 145 | }); 146 | 147 | it('should set the headers', async () => { 148 | const req = new MockRequest(); 149 | const res = new MockResponse(); 150 | 151 | req.method('PUT').url('https://httpbin.org/put'); 152 | await proxy(req, res); 153 | 154 | expect(res.headers()).toEqual( 155 | expect.objectContaining({ 156 | 'content-type': 'application/json', 157 | 'content-length': expect.any(String) 158 | }) 159 | ); 160 | }); 161 | 162 | it('should set the body', async () => { 163 | const req = new MockRequest(); 164 | const res = new MockResponse(); 165 | 166 | req.method('PUT').url('http://httpbin.org/put'); 167 | await proxy(req, res); 168 | 169 | expect(res.body()).toBeDefined(); 170 | }); 171 | 172 | it('should error', async () => { 173 | expect.assertions(1); 174 | const req = new MockRequest(); 175 | const res = new MockResponse(); 176 | 177 | req.method('DELETE').url('invalid://blah'); 178 | 179 | try { 180 | await proxy(req, res); 181 | } catch (error) { 182 | expect(error).not.toBeUndefined(); 183 | } 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/proxy.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | import * as https from 'https'; 3 | import MockRequest from './MockRequest'; 4 | import MockResponse from './MockResponse'; 5 | 6 | export default function( 7 | req: MockRequest, 8 | res: MockResponse 9 | ): Promise { 10 | return new Promise((resolve, reject) => { 11 | const options = { 12 | method: req.method(), 13 | protocol: `${req.url().protocol}:`, 14 | hostname: req.url().host, 15 | port: req.url().port, 16 | auth: req.url().username && `${req.url().username || ''}${req.url().password && ` ${req.url().password || ''}` || ''}`, 17 | path: req.url().path, 18 | headers: req.headers() 19 | }; 20 | 21 | const requestFn = 22 | req.url().protocol === 'https' ? https.request : http.request; 23 | 24 | const httpReq = requestFn(options, httpRes => { 25 | res.status(httpRes.statusCode || 0).reason(httpRes.statusMessage || ''); 26 | 27 | Object.keys(httpRes.headers).forEach(name => { 28 | const value = httpRes.headers[name]; 29 | res.header(name, Array.isArray(value) ? value[0] : value || ''); 30 | }); 31 | 32 | let resBody = ''; 33 | httpRes.setEncoding('utf8'); 34 | httpRes.on('data', chunk => { 35 | resBody += chunk.toString(); 36 | }); 37 | httpRes.on('end', () => { 38 | res.body(resBody); 39 | resolve(res); 40 | }); 41 | }); 42 | 43 | httpReq.on('error', reject); 44 | 45 | const reqBody = req.body(); 46 | if (reqBody !== undefined && reqBody !== null) { 47 | httpReq.write(reqBody); 48 | } 49 | httpReq.end(); 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/types.ts: -------------------------------------------------------------------------------- 1 | import {MockHeaders} from './MockHeaders'; 2 | import MockRequest from './MockRequest'; 3 | import MockResponse from './MockResponse'; 4 | 5 | export {MockHeaders}; 6 | 7 | export type MockObject = { 8 | status?: number; 9 | reason?: string; 10 | headers?: MockHeaders; 11 | body?: any; 12 | }; 13 | 14 | export type MockFunction = ( 15 | request: MockRequest, 16 | response: MockResponse 17 | ) => undefined | MockResponse | Promise; 18 | 19 | export type Mock = MockObject | MockFunction; 20 | 21 | export interface ErrorCallbackEvent { 22 | req: MockRequest; 23 | err: Error; 24 | } 25 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/utils/delay.test.ts: -------------------------------------------------------------------------------- 1 | import MockRequest from '../MockRequest'; 2 | import MockResponse from '../MockResponse'; 3 | import {delay} from './delay'; 4 | 5 | const ms = 250; 6 | 7 | describe('delay()', () => { 8 | it('should not delay the response when the handler does not return a response', async () => { 9 | const start = Date.now(); 10 | const res = await delay(() => undefined, ms)( 11 | new MockRequest(), 12 | new MockResponse() 13 | ); 14 | const finish = Date.now(); 15 | expect(finish - start).toBeLessThanOrEqual(ms); 16 | expect(res).not.toBeInstanceOf(MockResponse); 17 | expect(res).toBeUndefined(); 18 | }); 19 | 20 | it('should not delay the response when the handler does not resolve a response', async () => { 21 | const start = Date.now(); 22 | const res = await delay(() => Promise.resolve(undefined), ms)( 23 | new MockRequest(), 24 | new MockResponse() 25 | ); 26 | const finish = Date.now(); 27 | expect(finish - start).toBeLessThanOrEqual(ms); 28 | expect(res).not.toBeInstanceOf(MockResponse); 29 | expect(res).toBeUndefined(); 30 | }); 31 | 32 | it('should delay the response when the handler returns a response', async () => { 33 | const start = Date.now(); 34 | const res = await delay( 35 | (req, res) => res.status(201).body('Hello World!'), 36 | ms 37 | )(new MockRequest(), new MockResponse()); 38 | const finish = Date.now(); 39 | expect(finish - start).toBeGreaterThanOrEqual(ms); 40 | expect(res).toBeInstanceOf(MockResponse); 41 | if (res) { 42 | expect(res.status()).toEqual(201); 43 | expect(res.body()).toEqual('Hello World!'); 44 | } 45 | }); 46 | 47 | it('should delay the response when the handler does resolve a response', async () => { 48 | const start = Date.now(); 49 | const res = await delay( 50 | (req, res) => Promise.resolve(res.status(201).body('Hello World!')), 51 | ms 52 | )(new MockRequest(), new MockResponse()); 53 | const finish = Date.now(); 54 | expect(finish - start).toBeGreaterThanOrEqual(ms); 55 | expect(res).toBeInstanceOf(MockResponse); 56 | if (res) { 57 | expect(res.status()).toEqual(201); 58 | expect(res.body()).toEqual('Hello World!'); 59 | } 60 | }); 61 | 62 | it('should delay the response when the handler is a response object', async () => { 63 | const start = Date.now(); 64 | const res = await delay({status: 201, body: 'Hello World!'}, ms)( 65 | new MockRequest(), 66 | new MockResponse() 67 | ); 68 | const finish = Date.now(); 69 | expect(finish - start).toBeGreaterThanOrEqual(ms); 70 | expect(res).toBeInstanceOf(MockResponse); 71 | if (res) { 72 | expect(res.status()).toEqual(201); 73 | expect(res.body()).toEqual('Hello World!'); 74 | } 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/utils/delay.ts: -------------------------------------------------------------------------------- 1 | import {MockFunction, MockObject} from '../types'; 2 | import MockResponse from '../MockResponse'; 3 | import {createResponseFromObject} from '../createResponseFromObject'; 4 | 5 | export function delay( 6 | mock: MockFunction | MockObject, 7 | ms: number = 1500 8 | ): MockFunction { 9 | return (req, res) => { 10 | const ret = 11 | typeof mock === 'function' 12 | ? mock(req, res) 13 | : createResponseFromObject(mock); 14 | if (ret === undefined) { 15 | return undefined; 16 | } 17 | return Promise.resolve(ret).then(val => { 18 | if (val == undefined) { 19 | return undefined; 20 | } else { 21 | return new Promise(resolve => 22 | setTimeout(() => resolve(val), ms) 23 | ); 24 | } 25 | }); 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/utils/once.test.ts: -------------------------------------------------------------------------------- 1 | import MockRequest from '../MockRequest'; 2 | import MockResponse from '../MockResponse'; 3 | import {once} from './once'; 4 | 5 | describe('once()', () => { 6 | it('should only return a response when inited with a mock function and called for the first time', async () => { 7 | const handler = once((req, res) => res.status(201).body('Hello World!')); 8 | const first = await handler(new MockRequest(), new MockResponse()); 9 | const second = await handler(new MockRequest(), new MockResponse()); 10 | expect(first).toBeInstanceOf(MockResponse); 11 | if (first) { 12 | expect(first.status()).toEqual(201); 13 | expect(first.body()).toEqual('Hello World!'); 14 | } 15 | expect(second).toBeUndefined(); 16 | }); 17 | 18 | it('should only return a response when inited with a mock object and called for the first time', async () => { 19 | const handler = once({status: 201, body: 'Hello World!'}); 20 | const first = await handler(new MockRequest(), new MockResponse()); 21 | const second = await handler(new MockRequest(), new MockResponse()); 22 | expect(first).toBeInstanceOf(MockResponse); 23 | if (first) { 24 | expect(first.status()).toEqual(201); 25 | expect(first.body()).toEqual('Hello World!'); 26 | } 27 | expect(second).toBeUndefined(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/utils/once.ts: -------------------------------------------------------------------------------- 1 | import {MockFunction, MockObject} from '../types'; 2 | import {sequence} from './sequence'; 3 | 4 | export function once(mock: MockFunction | MockObject): MockFunction { 5 | return sequence([mock]); 6 | } 7 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/utils/sequence.test.ts: -------------------------------------------------------------------------------- 1 | import MockRequest from '../MockRequest'; 2 | import MockResponse from '../MockResponse'; 3 | import {sequence} from './sequence'; 4 | 5 | describe('sequence()', () => { 6 | it('should return undefined response when inited with empty array ', async () => { 7 | const handler = sequence([]); 8 | const first = await handler(new MockRequest(), new MockResponse()); 9 | 10 | expect(first).toBeUndefined(); 11 | }); 12 | 13 | it('should return a response when inited with two mocks and called for the first two time', async () => { 14 | const handler = sequence([ 15 | (req, res) => res.status(201).body('Hello'), 16 | {status: 201, body: 'World!'} 17 | ]); 18 | const first = await handler(new MockRequest(), new MockResponse()); 19 | const second = await handler(new MockRequest(), new MockResponse()); 20 | const third = await handler(new MockRequest(), new MockResponse()); 21 | expect(first).toBeInstanceOf(MockResponse); 22 | if (first) { 23 | expect(first.status()).toEqual(201); 24 | expect(first.body()).toEqual('Hello'); 25 | } 26 | expect(second).toBeInstanceOf(MockResponse); 27 | if (second) { 28 | expect(second.status()).toEqual(201); 29 | expect(second.body()).toEqual('World!'); 30 | } 31 | expect(third).toBeUndefined(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/xhr-mock/src/utils/sequence.ts: -------------------------------------------------------------------------------- 1 | import {MockFunction, MockObject} from '../types'; 2 | import {createResponseFromObject} from '../createResponseFromObject'; 3 | 4 | export function sequence(mocks: (MockFunction | MockObject)[]): MockFunction { 5 | let callCount = 0; 6 | 7 | return (req, res) => { 8 | if (callCount < mocks.length) { 9 | const mock = mocks[callCount++]; 10 | return typeof mock === 'function' 11 | ? mock(req, res) 12 | : createResponseFromObject(mock); 13 | } 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /packages/xhr-mock/test/acceptance.test.ts: -------------------------------------------------------------------------------- 1 | import mock, {MockResponse} from '../src'; 2 | import { 3 | recordXHREventsAsync, 4 | recordXHREventsSync 5 | } from './util/recordXHREvents'; 6 | 7 | // the expected output of these tests is recorded using: 8 | // => https://codepen.io/jameslnewell/pen/RxQqzV?editors=0010 9 | describe('xhr-mock', () => { 10 | beforeEach(() => mock.setup()); 11 | afterEach(() => mock.teardown()); 12 | 13 | describe('async=false', () => { 14 | it('when the request downloads some data', () => { 15 | mock.get('/', { 16 | status: 200, 17 | headers: { 18 | 'Content-Length': '12' 19 | }, 20 | body: 'Hello World!' 21 | }); 22 | 23 | const xhr = new XMLHttpRequest(); 24 | const events = recordXHREventsSync(xhr); 25 | xhr.open('GET', '/', false); 26 | xhr.send(); 27 | 28 | expect(events).toEqual([ 29 | ['readystatechange', 1], 30 | ['readystatechange', 4], 31 | [ 32 | 'load', 33 | { 34 | lengthComputable: true, 35 | loaded: 12, 36 | total: 12 37 | } 38 | ], 39 | [ 40 | 'loadend', 41 | { 42 | lengthComputable: true, 43 | loaded: 12, 44 | total: 12 45 | } 46 | ] 47 | ]); 48 | }); 49 | 50 | it('when the request errored', () => { 51 | expect.assertions(1); 52 | 53 | mock.error(() => { 54 | /* do nothing */ 55 | }); 56 | mock.get('/', () => { 57 | throw new Error('😵'); 58 | }); 59 | 60 | const xhr = new XMLHttpRequest(); 61 | const events = recordXHREventsSync(xhr); 62 | xhr.open('GET', '/', false); 63 | 64 | try { 65 | xhr.send(); 66 | } catch (err) { 67 | expect(events).toEqual([['readystatechange', 1]]); 68 | } 69 | }); 70 | }); 71 | 72 | describe('async=true', () => { 73 | it('when the request downloads data', () => { 74 | mock.get('/', { 75 | status: 200, 76 | headers: { 77 | 'Content-Length': '12' 78 | }, 79 | body: 'Hello World!' 80 | }); 81 | 82 | const xhr = new XMLHttpRequest(); 83 | const events = recordXHREventsAsync(xhr); 84 | 85 | xhr.open('GET', '/'); 86 | xhr.send(); 87 | 88 | return expect(events).resolves.toEqual([ 89 | ['readystatechange', 1], 90 | [ 91 | 'loadstart', 92 | { 93 | lengthComputable: false, 94 | loaded: 0, 95 | total: 0 96 | } 97 | ], 98 | ['readystatechange', 2], 99 | ['readystatechange', 3], 100 | [ 101 | 'progress', 102 | { 103 | lengthComputable: true, 104 | loaded: 12, 105 | total: 12 106 | } 107 | ], 108 | ['readystatechange', 4], 109 | [ 110 | 'load', 111 | { 112 | lengthComputable: true, 113 | loaded: 12, 114 | total: 12 115 | } 116 | ], 117 | [ 118 | 'loadend', 119 | { 120 | lengthComputable: true, 121 | loaded: 12, 122 | total: 12 123 | } 124 | ] 125 | ]); 126 | }); 127 | 128 | it('when the request uploads data', () => { 129 | mock.post('/', { 130 | status: 200, 131 | headers: { 132 | 'Content-Length': '12' 133 | }, 134 | body: 'Hello World!' 135 | }); 136 | 137 | const xhr = new XMLHttpRequest(); 138 | const events = recordXHREventsAsync(xhr); 139 | xhr.open('POST', '/'); 140 | xhr.setRequestHeader('Content-Length', '6'); 141 | xhr.send('foobar'); 142 | 143 | return expect(events).resolves.toEqual([ 144 | ['readystatechange', 1], 145 | [ 146 | 'loadstart', 147 | { 148 | lengthComputable: false, 149 | loaded: 0, 150 | total: 0 151 | } 152 | ], 153 | [ 154 | 'upload:loadstart', 155 | { 156 | lengthComputable: true, 157 | loaded: 0, 158 | total: 6 159 | } 160 | ], 161 | [ 162 | 'upload:progress', 163 | { 164 | lengthComputable: true, 165 | loaded: 6, 166 | total: 6 167 | } 168 | ], 169 | [ 170 | 'upload:load', 171 | { 172 | lengthComputable: true, 173 | loaded: 6, 174 | total: 6 175 | } 176 | ], 177 | [ 178 | 'upload:loadend', 179 | { 180 | lengthComputable: true, 181 | loaded: 6, 182 | total: 6 183 | } 184 | ], 185 | ['readystatechange', 2], 186 | ['readystatechange', 3], 187 | [ 188 | 'progress', 189 | { 190 | lengthComputable: true, 191 | loaded: 12, 192 | total: 12 193 | } 194 | ], 195 | ['readystatechange', 4], 196 | [ 197 | 'load', 198 | { 199 | lengthComputable: true, 200 | loaded: 12, 201 | total: 12 202 | } 203 | ], 204 | [ 205 | 'loadend', 206 | { 207 | lengthComputable: true, 208 | loaded: 12, 209 | total: 12 210 | } 211 | ] 212 | ]); 213 | }); 214 | 215 | it('when the request timed out', () => { 216 | mock.get('/', () => new Promise(() => {})); 217 | 218 | const xhr = new XMLHttpRequest(); 219 | const events = recordXHREventsAsync(xhr); 220 | xhr.timeout = 1; 221 | xhr.open('GET', '/'); 222 | xhr.send(); 223 | 224 | return expect(events).resolves.toEqual([ 225 | ['readystatechange', 1], 226 | [ 227 | 'loadstart', 228 | { 229 | lengthComputable: false, 230 | loaded: 0, 231 | total: 0 232 | } 233 | ], 234 | ['readystatechange', 4], 235 | 'timeout', 236 | [ 237 | 'loadend', 238 | { 239 | lengthComputable: false, 240 | loaded: 0, 241 | total: 0 242 | } 243 | ] 244 | ]); 245 | }); 246 | 247 | it('when the request aborted', () => { 248 | mock.get('/', () => new Promise(() => {})); 249 | 250 | const xhr = new XMLHttpRequest(); 251 | const events = recordXHREventsAsync(xhr); 252 | xhr.open('GET', '/'); 253 | xhr.send(); 254 | xhr.abort(); 255 | 256 | return expect(events).resolves.toEqual([ 257 | ['readystatechange', 1], 258 | [ 259 | 'loadstart', 260 | { 261 | lengthComputable: false, 262 | loaded: 0, 263 | total: 0 264 | } 265 | ], 266 | ['readystatechange', 4], 267 | 'abort', 268 | [ 269 | 'loadend', 270 | { 271 | lengthComputable: false, 272 | loaded: 0, 273 | total: 0 274 | } 275 | ] 276 | ]); 277 | }); 278 | 279 | it('when the request errored', () => { 280 | mock.error(() => { 281 | /* do nothing */ 282 | }); 283 | mock.get('/', () => Promise.reject(new Error('😵'))); 284 | 285 | const xhr = new XMLHttpRequest(); 286 | const events = recordXHREventsAsync(xhr); 287 | xhr.open('GET', '/'); 288 | xhr.send(); 289 | 290 | return expect(events).resolves.toEqual([ 291 | ['readystatechange', 1], 292 | [ 293 | 'loadstart', 294 | { 295 | lengthComputable: false, 296 | loaded: 0, 297 | total: 0 298 | } 299 | ], 300 | ['readystatechange', 4], 301 | 'error', 302 | [ 303 | 'loadend', 304 | { 305 | lengthComputable: false, 306 | loaded: 0, 307 | total: 0 308 | } 309 | ] 310 | ]); 311 | }); 312 | }); 313 | }); 314 | -------------------------------------------------------------------------------- /packages/xhr-mock/test/howto.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file contains the source from the "How to?" section of README.md 3 | */ 4 | 5 | import mock, {proxy} from '../src'; 6 | 7 | describe('how to', () => { 8 | beforeEach(() => mock.setup()); 9 | 10 | afterEach(() => mock.teardown()); 11 | 12 | it('should report upload progress', async () => { 13 | mock.post('/', {}); 14 | 15 | const xhr = new XMLHttpRequest(); 16 | xhr.upload.onprogress = event => 17 | console.log('upload progress:', event.loaded, '/', event.total); 18 | xhr.open('POST', '/'); 19 | xhr.setRequestHeader('Content-Length', '12'); 20 | xhr.send('Hello World!'); 21 | }); 22 | 23 | it('should report download progress', async () => { 24 | mock.get('/', { 25 | headers: {'Content-Length': '12'}, 26 | body: 'Hello World!' 27 | }); 28 | 29 | const xhr = new XMLHttpRequest(); 30 | xhr.onprogress = event => 31 | console.log('download progress:', event.loaded, '/', event.total); 32 | xhr.open('GET', '/'); 33 | xhr.send(); 34 | }); 35 | 36 | it('should simulate a timeout', async () => { 37 | mock.get('/', () => new Promise(() => {})); 38 | 39 | const xhr = new XMLHttpRequest(); 40 | xhr.timeout = 100; 41 | xhr.ontimeout = event => console.log('timed out'); 42 | xhr.open('GET', '/'); 43 | xhr.send(); 44 | }); 45 | 46 | it('should simulate an error', async () => { 47 | mock.error(() => { 48 | /* do nothing */ 49 | }); 50 | mock.get('/', () => Promise.reject(new Error('😵'))); 51 | 52 | const xhr = new XMLHttpRequest(); 53 | xhr.onerror = event => console.log('error'); 54 | xhr.open('GET', '/'); 55 | xhr.send(); 56 | }); 57 | 58 | it('should proxy requests', async () => { 59 | // mock specific requests 60 | mock.post('/', {status: 204}); 61 | 62 | // proxy unhandled requests to the real servers 63 | mock.use(proxy); 64 | 65 | // this request will be mocked 66 | const xhr1 = new XMLHttpRequest(); 67 | xhr1.open('POST', '/'); 68 | xhr1.send(); 69 | 70 | // this request will be proxied to the real server 71 | const xhr2 = new XMLHttpRequest(); 72 | xhr2.open('GET', 'https://jsonplaceholder.typicode.com/users/1'); 73 | xhr2.send(); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /packages/xhr-mock/test/integration.test.ts: -------------------------------------------------------------------------------- 1 | import mock, {proxy} from '../src'; 2 | 3 | const request = (method: string, url: string): Promise => 4 | new Promise((resolve, reject) => { 5 | const xhr = new XMLHttpRequest(); 6 | xhr.onreadystatechange = () => { 7 | if (xhr.readyState == XMLHttpRequest.DONE) { 8 | resolve(xhr.responseText); 9 | } 10 | }; 11 | xhr.onerror = event => reject(event.error); 12 | xhr.open(method, url); 13 | xhr.send(); 14 | }); 15 | 16 | describe('integration', () => { 17 | beforeEach(() => mock.setup()); 18 | afterEach(() => mock.teardown()); 19 | 20 | it('should send a POST request', () => { 21 | mock.use((req, res) => { 22 | expect(req.method()).toEqual('POST'); 23 | expect(req.url().toString()).toEqual('/example'); 24 | return res; 25 | }); 26 | 27 | return request('POST', '/example'); 28 | }); 29 | 30 | it('should timeout when the timeout is greater than 0', done => { 31 | mock.post('http://localhost/foo/bar', (req, res) => { 32 | return new Promise(() => {}); 33 | }); 34 | 35 | const xhr = new XMLHttpRequest(); 36 | xhr.timeout = 100; 37 | xhr.ontimeout = () => done(); 38 | xhr.onerror = () => done.fail(); 39 | xhr.open('POST', 'http://localhost/foo/bar'); 40 | xhr.send(); 41 | }); 42 | 43 | it('should error when the promise is rejected', done => { 44 | mock.post('http://localhost/foo/bar', (req, res) => { 45 | return Promise.reject(new Error('Uh oh')); 46 | }); 47 | 48 | const xhr = new XMLHttpRequest(); 49 | xhr.onerror = () => done(); 50 | xhr.open('post', 'http://localhost/foo/bar'); 51 | xhr.send(); 52 | }); 53 | 54 | it('should emit progress events when uploading', done => { 55 | expect.assertions(1); 56 | mock.post('http://localhost/foo/bar', (req, res) => { 57 | return res 58 | .status(201) 59 | .header('Content-Type', 'image/jpeg') 60 | .body(''); 61 | }); 62 | 63 | const xhr = new XMLHttpRequest(); 64 | xhr.upload.onprogress = jest.fn(); 65 | xhr.onerror = () => done.fail(); 66 | xhr.onloadend = () => { 67 | expect(xhr.upload.onprogress).toHaveBeenCalledTimes(1); 68 | done(); 69 | }; 70 | xhr.open('POST', 'http://localhost/foo/bar'); 71 | xhr.send('Hello World!'); 72 | }); 73 | 74 | it('should emit progress events when downloading', done => { 75 | expect.assertions(1); 76 | mock.post('http://localhost/foo/bar', (req, res) => { 77 | return res 78 | .status(201) 79 | .header('Content-Type', 'image/jpeg') 80 | .body('Hello World!'); 81 | }); 82 | 83 | const xhr = new XMLHttpRequest(); 84 | xhr.onprogress = jest.fn(); 85 | xhr.onerror = () => done.fail(); 86 | xhr.onloadend = () => { 87 | expect(xhr.onprogress).toHaveBeenCalledTimes(1); 88 | done(); 89 | }; 90 | xhr.open('POST', 'http://localhost/foo/bar'); 91 | xhr.send(); 92 | }); 93 | 94 | //TODO: test content-length 95 | 96 | it('should proxy unhandled URLs', async () => { 97 | jest.setTimeout(20000); 98 | 99 | mock.get('https://reqres.in/api/users/1', { 100 | status: 200, 101 | body: 'Hello World!' 102 | }); 103 | 104 | mock.use(proxy); 105 | 106 | const ret1 = await request('GET', 'https://reqres.in/api/users/1'); 107 | expect(ret1).toEqual('Hello World!'); 108 | 109 | const ret2 = await request('GET', 'https://reqres.in/api/users/2'); 110 | expect(JSON.parse(ret2)).toEqual({ 111 | data: expect.objectContaining({ 112 | id: 2, 113 | first_name: 'Janet' 114 | }) 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /packages/xhr-mock/test/usage.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file contains the source from the "Usage" section of README.md 3 | */ 4 | 5 | import mock from '../src'; 6 | 7 | function createUser(data) { 8 | return new Promise((resolve, reject) => { 9 | const xhr = new XMLHttpRequest(); 10 | xhr.onreadystatechange = () => { 11 | if (xhr.readyState == XMLHttpRequest.DONE) { 12 | if (xhr.status === 201) { 13 | try { 14 | resolve(JSON.parse(xhr.responseText).data); 15 | } catch (error) { 16 | reject(error); 17 | } 18 | } else if (xhr.status) { 19 | try { 20 | reject(JSON.parse(xhr.responseText).error); 21 | } catch (error) { 22 | reject(error); 23 | } 24 | } else { 25 | reject(new Error('An error ocurred whilst sending the request.')); 26 | } 27 | } 28 | }; 29 | xhr.open('post', '/api/user'); 30 | xhr.setRequestHeader('Content-Type', 'application/json'); 31 | xhr.send(JSON.stringify({data: data})); 32 | }); 33 | } 34 | 35 | describe('createUser()', () => { 36 | // replace the real XHR object with the mock XHR object before each test 37 | beforeEach(() => mock.setup()); 38 | 39 | // put the real XHR object back and clear the mocks after each test 40 | afterEach(() => mock.teardown()); 41 | 42 | it('should send the data as JSON', async () => { 43 | expect.assertions(2); 44 | 45 | mock.post('/api/user', (req, res) => { 46 | expect(req.header('Content-Type')).toEqual('application/json'); 47 | expect(req.body()).toEqual('{"data":{"name":"John"}}'); 48 | return res.status(201).body('{"data":{"id":"abc-123"}}'); 49 | }); 50 | 51 | await createUser({name: 'John'}); 52 | }); 53 | 54 | it('should resolve with some data when status=201', async () => { 55 | expect.assertions(1); 56 | 57 | mock.post('/api/user', { 58 | status: 201, 59 | reason: 'Created', 60 | body: '{"data":{"id":"abc-123"}}' 61 | }); 62 | 63 | const user = await createUser({name: 'John'}); 64 | 65 | expect(user).toEqual({id: 'abc-123'}); 66 | }); 67 | 68 | it('should reject with an error when status=400', async () => { 69 | expect.assertions(1); 70 | 71 | mock.post('/api/user', { 72 | status: 400, 73 | reason: 'Bad request', 74 | body: '{"error":"A user named \\"John\\" already exists."}' 75 | }); 76 | 77 | try { 78 | const user = await createUser({name: 'John'}); 79 | } catch (error) { 80 | expect(error).toMatch('A user named "John" already exists.'); 81 | } 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /packages/xhr-mock/test/util/recordXHREvents.test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jameslnewell/xhr-mock/b4956127059c3d2429df7e4be8b2536df2f42410/packages/xhr-mock/test/util/recordXHREvents.test.js -------------------------------------------------------------------------------- /packages/xhr-mock/test/util/recordXHREvents.ts: -------------------------------------------------------------------------------- 1 | export function recordXHREventsAsync(xhr: XMLHttpRequest) { 2 | return new Promise(resolve => { 3 | const events = []; 4 | 5 | const addErrorEventListener = (type: string) => { 6 | xhr.addEventListener(type, () => { 7 | events.push(type); 8 | resolve(events); 9 | }); 10 | }; 11 | 12 | const addDownloadProgressEventListener = (type: string) => { 13 | xhr.addEventListener(type, (event: ProgressEvent) => { 14 | const {lengthComputable, loaded, total} = event; 15 | events.push([ 16 | type, 17 | { 18 | lengthComputable, 19 | loaded, 20 | total 21 | } 22 | ]); 23 | if (type === 'loadend') { 24 | resolve(events); 25 | } 26 | }); 27 | }; 28 | 29 | const addUploadProgressEventListener = (type: string) => { 30 | xhr.upload.addEventListener(type, (event: ProgressEvent) => { 31 | const {lengthComputable, loaded, total} = event; 32 | events.push([ 33 | `upload:${type}`, 34 | { 35 | lengthComputable, 36 | loaded, 37 | total 38 | } 39 | ]); 40 | }); 41 | }; 42 | 43 | addErrorEventListener('abort'); 44 | addErrorEventListener('error'); 45 | addErrorEventListener('timeout'); 46 | 47 | addDownloadProgressEventListener('loadstart'); 48 | addDownloadProgressEventListener('progress'); 49 | addDownloadProgressEventListener('load'); 50 | addDownloadProgressEventListener('loadend'); 51 | 52 | addUploadProgressEventListener('loadstart'); 53 | addUploadProgressEventListener('progress'); 54 | addUploadProgressEventListener('load'); 55 | addUploadProgressEventListener('loadend'); 56 | 57 | xhr.addEventListener('readystatechange', () => { 58 | events.push(['readystatechange', xhr.readyState]); 59 | }); 60 | }); 61 | } 62 | 63 | export function recordXHREventsSync(xhr: XMLHttpRequest) { 64 | const events = []; 65 | 66 | const addErrorEventListener = (type: string) => { 67 | xhr.addEventListener(type, () => { 68 | events.push(type); 69 | }); 70 | }; 71 | 72 | const addDownloadProgressEventListener = (type: string) => { 73 | xhr.addEventListener(type, (event: ProgressEvent) => { 74 | const {lengthComputable, loaded, total} = event; 75 | events.push([ 76 | type, 77 | { 78 | lengthComputable, 79 | loaded, 80 | total 81 | } 82 | ]); 83 | }); 84 | }; 85 | 86 | const addUploadProgressEventListener = (type: string) => { 87 | xhr.upload.addEventListener(type, (event: ProgressEvent) => { 88 | const {lengthComputable, loaded, total} = event; 89 | events.push([ 90 | `upload:${type}`, 91 | { 92 | lengthComputable, 93 | loaded, 94 | total 95 | } 96 | ]); 97 | }); 98 | }; 99 | 100 | addErrorEventListener('abort'); 101 | addErrorEventListener('error'); 102 | addErrorEventListener('timeout'); 103 | 104 | addDownloadProgressEventListener('loadstart'); 105 | addDownloadProgressEventListener('progress'); 106 | addDownloadProgressEventListener('load'); 107 | addDownloadProgressEventListener('loadend'); 108 | 109 | addUploadProgressEventListener('loadstart'); 110 | addUploadProgressEventListener('progress'); 111 | addUploadProgressEventListener('load'); 112 | addUploadProgressEventListener('loadend'); 113 | 114 | xhr.addEventListener('readystatechange', () => { 115 | events.push(['readystatechange', xhr.readyState]); 116 | }); 117 | 118 | return events; 119 | } 120 | -------------------------------------------------------------------------------- /packages/xhr-mock/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | }, 6 | "files": [ 7 | "typings/global.d.ts", 8 | "typings/url.d.ts" 9 | ], 10 | "include": [ 11 | "src/**/*" 12 | ], 13 | "exclude": [ 14 | "node_modules", 15 | "src/**/*.test.*" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/xhr-mock/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "src/**/*" 5 | ], 6 | "exclude": [ 7 | "node_modules" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/xhr-mock/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'global' { 2 | export let XMLHttpRequest: {new (): XMLHttpRequest}; 3 | } 4 | -------------------------------------------------------------------------------- /packages/xhr-mock/typings/url.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'url'; 2 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es5", "es2015", "dom"], 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "noImplicitAny": true, 7 | "strictNullChecks": true, 8 | "target": "es5", 9 | "skipLibCheck": true 10 | }, 11 | "exclude": ["node_modules"] 12 | } 13 | -------------------------------------------------------------------------------- /tslint.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "rules": { 4 | "no-debugger": "error", 5 | "mocha-avoid-only": "error" 6 | }, 7 | "rulesDirectory": [ 8 | "node_modules/tslint-microsoft-contrib" 9 | ] 10 | } 11 | --------------------------------------------------------------------------------