├── .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 | []()
4 | [](https://travis-ci.org/jameslnewell/xhr-mock)
5 | []()
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 |
--------------------------------------------------------------------------------