├── .tool-versions
├── test
├── resources
│ └── xhr_target.txt
└── global-hack.js
├── .eslintignore
├── .gitignore
├── .prettierignore
├── mkdocs.yml
├── .browserslistrc
├── .prettierrc
├── lib
├── fake-xhr
│ ├── blob.js
│ └── index.js
├── index.js
├── event
│ ├── index.js
│ ├── custom-event.js
│ ├── event.js
│ ├── progress-event.js
│ ├── event-target.js
│ └── index.test.js
├── fake-server
│ ├── log.js
│ ├── log.test.js
│ ├── fake-server-with-clock.js
│ ├── fake-server-with-clock.test.js
│ ├── index.js
│ └── index.test.js
├── index.test.js
└── configure-logger
│ ├── index.js
│ └── index.test.js
├── .eslintrc.yml
├── .editorconfig
├── .github
├── stale.yml
├── CONTRIBUTING.md
└── workflows
│ └── main.yml
├── LICENSE
├── package.json
├── CODE_OF_CONDUCT.md
├── README.md
└── docs
└── index.md
/.tool-versions:
--------------------------------------------------------------------------------
1 | nodejs 16.20.2
2 |
--------------------------------------------------------------------------------
/test/resources/xhr_target.txt:
--------------------------------------------------------------------------------
1 | loaded successfully
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | coverage/
2 | site/
3 | nise.js
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | nise.js
3 | coverage/
4 | site/
5 | .nyc_output/
6 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | nise.js
2 | coverage/
3 | site/
4 | .nyc_output/
5 | History.md
6 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: nise
2 | theme: readthedocs
3 | repo_url: https://github.com/sinonjs/nise/
4 |
--------------------------------------------------------------------------------
/test/global-hack.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | if (typeof global === "undefined") {
4 | window.global = window;
5 | }
6 |
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 0.5%
2 | last 2 versions
3 | Firefox ESR
4 | not dead
5 | not IE 11
6 | not op_mini all
7 | maintained node versions
8 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | # This project uses Prettier defaults, the config file is present in
2 | # order to trigger editor plugins to allow format on save behaviour
3 |
--------------------------------------------------------------------------------
/lib/fake-xhr/blob.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | exports.isSupported = (function () {
4 | try {
5 | return Boolean(new Blob());
6 | } catch (e) {
7 | return false;
8 | }
9 | })();
10 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | module.exports = {
4 | fakeServer: require("./fake-server"),
5 | fakeServerWithClock: require("./fake-server/fake-server-with-clock"),
6 | fakeXhr: require("./fake-xhr"),
7 | };
8 |
--------------------------------------------------------------------------------
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | extends:
2 | - "@sinonjs/eslint-config"
3 |
4 | globals:
5 | ArrayBuffer: false
6 | Uint8Array: false
7 |
8 | overrides:
9 | - files: "*.test.*"
10 | rules:
11 | max-nested-callbacks: off
12 |
--------------------------------------------------------------------------------
/lib/event/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | module.exports = {
4 | Event: require("./event"),
5 | ProgressEvent: require("./progress-event"),
6 | CustomEvent: require("./custom-event"),
7 | EventTarget: require("./event-target"),
8 | };
9 |
--------------------------------------------------------------------------------
/lib/event/custom-event.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var Event = require("./event");
4 |
5 | function CustomEvent(type, customData, target) {
6 | this.initEvent(type, false, false, target);
7 | this.detail = customData.detail || null;
8 | }
9 |
10 | CustomEvent.prototype = new Event();
11 |
12 | CustomEvent.prototype.constructor = CustomEvent;
13 |
14 | module.exports = CustomEvent;
15 |
--------------------------------------------------------------------------------
/lib/fake-server/log.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var inspect = require("util").inspect;
3 |
4 | function log(response, request) {
5 | var str;
6 |
7 | str = `Request:\n${inspect(request)}\n\n`;
8 | str += `Response:\n${inspect(response)}\n\n`;
9 |
10 | /* istanbul ignore else: when this.logger is not a function, it can't be called */
11 | if (typeof this.logger === "function") {
12 | this.logger(str);
13 | }
14 | }
15 |
16 | module.exports = log;
17 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | ; EditorConfig file: http://EditorConfig.org
2 | ; Install the "EditorConfig" plugin into your editor to use
3 |
4 | root = true
5 |
6 | [*]
7 | charset = utf-8
8 | end_of_line = lf
9 | insert_final_newline = true
10 | indent_style = space
11 | indent_size = 4
12 | trim_trailing_whitespace = true
13 |
14 | # Matches the exact files either package.json or .travis.yml
15 | [{package.json, .travis.yml}]
16 | indent_style = space
17 | indent_size = 2
18 |
19 | ; Needed if doing `git add --patch` to edit patches
20 | [*.diff]
21 | trim_trailing_whitespace = false
22 |
--------------------------------------------------------------------------------
/lib/event/event.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | function Event(type, bubbles, cancelable, target) {
4 | this.initEvent(type, bubbles, cancelable, target);
5 | }
6 |
7 | Event.prototype = {
8 | initEvent: function (type, bubbles, cancelable, target) {
9 | this.type = type;
10 | this.bubbles = bubbles;
11 | this.cancelable = cancelable;
12 | this.target = target;
13 | this.currentTarget = target;
14 | },
15 |
16 | // eslint-disable-next-line no-empty-function
17 | stopPropagation: function () {},
18 |
19 | preventDefault: function () {
20 | this.defaultPrevented = true;
21 | },
22 | };
23 |
24 | module.exports = Event;
25 |
--------------------------------------------------------------------------------
/lib/event/progress-event.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var Event = require("./event");
4 |
5 | function ProgressEvent(type, progressEventRaw, target) {
6 | this.initEvent(type, false, false, target);
7 | this.loaded =
8 | typeof progressEventRaw.loaded === "number"
9 | ? progressEventRaw.loaded
10 | : null;
11 | this.total =
12 | typeof progressEventRaw.total === "number"
13 | ? progressEventRaw.total
14 | : null;
15 | this.lengthComputable = Boolean(progressEventRaw.total);
16 | }
17 |
18 | ProgressEvent.prototype = new Event();
19 |
20 | ProgressEvent.prototype.constructor = ProgressEvent;
21 |
22 | module.exports = ProgressEvent;
23 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 60
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 7
5 | # Issues with these labels will never be considered stale
6 | exemptLabels:
7 | - pinned
8 | - security
9 | # Label to use when marking an issue as stale
10 | staleLabel: stale
11 | # Comment to post when marking an issue as stale. Set to `false` to disable
12 | markComment: >
13 | This issue has been automatically marked as stale because it has not had
14 | recent activity. It will be closed if no further activity occurs. Thank you
15 | for your contributions.
16 | # Comment to post when closing a stale issue. Set to `false` to disable
17 | closeComment: false
18 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | ## Contributor Code of Conduct
4 |
5 | Please note that this project is released with a [Contributor Code of Conduct](../CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.
6 |
7 | ## Use EditorConfig
8 |
9 | To save everyone some time, please use [EditorConfig](http://editorconfig.org), so your editor helps make
10 | sure we all use the same encoding, indentation, line endings, etc.
11 |
12 | ## Compatibility
13 |
14 | This repository follows the [compatibility guidelines of `sinon`](https://github.com/sinonjs/sinon/blob/master/CONTRIBUTING.md#compatibility)
15 |
16 | ## Style
17 |
18 | This repository follows the [style guidelines of `sinon`](https://github.com/sinonjs/sinon/blob/master/CONTRIBUTING.md#style)
19 |
--------------------------------------------------------------------------------
/lib/index.test.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var assert = require("@sinonjs/referee").assert;
4 | var api = require("./index");
5 |
6 | describe("api", function () {
7 | it("should export 'fake-server/' as fakeServer", function () {
8 | var expected = require("./fake-server");
9 | var actual = api.fakeServer;
10 |
11 | assert.equals(actual, expected);
12 | });
13 |
14 | it("should export 'fake-server/fake-server-with-clock' as fakeServerWithClock", function () {
15 | var expected = require("./fake-server/fake-server-with-clock");
16 | var actual = api.fakeServerWithClock;
17 |
18 | assert.equals(actual, expected);
19 | });
20 |
21 | it("should export 'fake-xhr/' as fakeXhr", function () {
22 | var expected = require("./fake-xhr");
23 | var actual = api.fakeXhr;
24 |
25 | assert.equals(actual, expected);
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | (The BSD License)
2 |
3 | Copyright (c) 2010-2017, Christian Johansen, christian@cjohansen.no
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without modification,
7 | are permitted provided that the following conditions are met:
8 |
9 | * Redistributions of source code must retain the above copyright notice,
10 | this list of conditions and the following disclaimer.
11 | * Redistributions in binary form must reproduce the above copyright notice,
12 | this list of conditions and the following disclaimer in the documentation
13 | and/or other materials provided with the distribution.
14 | * Neither the name of Christian Johansen nor the names of his contributors
15 | may be used to endorse or promote products derived from this software
16 | without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/lib/configure-logger/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | // cache a reference to setTimeout, so that our reference won't be stubbed out
4 | // when using fake timers and errors will still get logged
5 | // https://github.com/cjohansen/Sinon.JS/issues/381
6 | var realSetTimeout = setTimeout;
7 |
8 | function configureLogger(config) {
9 | // eslint-disable-next-line no-param-reassign
10 | config = config || {};
11 | // Function which prints errors.
12 | if (!config.hasOwnProperty("logger")) {
13 | // eslint-disable-next-line no-empty-function
14 | config.logger = function () {};
15 | }
16 | // When set to true, any errors logged will be thrown immediately;
17 | // If set to false, the errors will be thrown in separate execution frame.
18 | if (!config.hasOwnProperty("useImmediateExceptions")) {
19 | config.useImmediateExceptions = true;
20 | }
21 | // wrap realSetTimeout with something we can stub in tests
22 | if (!config.hasOwnProperty("setTimeout")) {
23 | config.setTimeout = realSetTimeout;
24 | }
25 |
26 | return function logError(label, e) {
27 | var msg = `${label} threw exception: `;
28 | var err = {
29 | name: e.name || label,
30 | message: e.message || e.toString(),
31 | stack: e.stack,
32 | };
33 |
34 | function throwLoggedError() {
35 | err.message = msg + err.message;
36 | throw err;
37 | }
38 |
39 | config.logger(`${msg}[${err.name}] ${err.message}`);
40 |
41 | if (err.stack) {
42 | config.logger(err.stack);
43 | }
44 |
45 | if (config.useImmediateExceptions) {
46 | throwLoggedError();
47 | } else {
48 | config.setTimeout(throwLoggedError, 0);
49 | }
50 | };
51 | }
52 |
53 | module.exports = configureLogger;
54 |
--------------------------------------------------------------------------------
/lib/fake-server/log.test.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var assert = require("@sinonjs/referee").assert;
4 | var proxyquire = require("proxyquire").noCallThru();
5 | var sinon = require("sinon");
6 |
7 | describe("log", function () {
8 | beforeEach(function () {
9 | this.fakeInspect = sinon.fake.returns(
10 | "a953421f-d933-4a20-87df-c4b3016177f3",
11 | );
12 |
13 | this.logFn = proxyquire("./log", {
14 | util: {
15 | inspect: this.fakeInspect,
16 | },
17 | });
18 | });
19 |
20 | context("when this.logger is defined", function () {
21 | beforeEach(function () {
22 | this.request = {};
23 | this.response = {};
24 |
25 | this.instance = {
26 | logger: sinon.fake(),
27 | };
28 |
29 | this.logFn.call(this.instance, this.request, this.response);
30 | });
31 |
32 | it("calls this.logger with a string", function () {
33 | assert.isTrue(this.instance.logger.calledOnce);
34 | assert.isString(this.instance.logger.args[0][0]);
35 | });
36 |
37 | it("formats the request argument", function () {
38 | assert.isTrue(this.fakeInspect.calledWith(this.request));
39 | });
40 |
41 | it("uses the formatted request argument", function () {
42 | assert.isTrue(
43 | this.instance.logger.args[0][0].includes(
44 | this.fakeInspect.returnValues[0],
45 | ),
46 | );
47 | });
48 |
49 | it("formats the response argument", function () {
50 | assert.isTrue(this.fakeInspect.calledWith(this.response));
51 | });
52 |
53 | it("uses the formatted response argument", function () {
54 | assert.isTrue(
55 | this.instance.logger.args[0][0].includes(
56 | this.fakeInspect.returnValues[1],
57 | ),
58 | );
59 | });
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/lib/fake-server/fake-server-with-clock.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var FakeTimers = require("@sinonjs/fake-timers");
4 | var fakeServer = require("./index");
5 |
6 | // eslint-disable-next-line no-empty-function
7 | function Server() {}
8 | Server.prototype = fakeServer;
9 |
10 | var fakeServerWithClock = new Server();
11 |
12 | fakeServerWithClock.addRequest = function addRequest(xhr) {
13 | if (xhr.async) {
14 | if (typeof setTimeout.clock === "object") {
15 | this.clock = setTimeout.clock;
16 | } else {
17 | this.clock = FakeTimers.install();
18 | this.resetClock = true;
19 | }
20 |
21 | if (!this.longestTimeout) {
22 | var clockSetTimeout = this.clock.setTimeout;
23 | var clockSetInterval = this.clock.setInterval;
24 | var server = this;
25 |
26 | this.clock.setTimeout = function (fn, timeout) {
27 | server.longestTimeout = Math.max(
28 | timeout,
29 | server.longestTimeout || 0,
30 | );
31 |
32 | return clockSetTimeout.apply(this, arguments);
33 | };
34 |
35 | this.clock.setInterval = function (fn, timeout) {
36 | server.longestTimeout = Math.max(
37 | timeout,
38 | server.longestTimeout || 0,
39 | );
40 |
41 | return clockSetInterval.apply(this, arguments);
42 | };
43 | }
44 | }
45 |
46 | return fakeServer.addRequest.call(this, xhr);
47 | };
48 |
49 | fakeServerWithClock.respond = function respond() {
50 | var returnVal = fakeServer.respond.apply(this, arguments);
51 |
52 | if (this.clock) {
53 | this.clock.tick(this.longestTimeout || 0);
54 | this.longestTimeout = 0;
55 |
56 | if (this.resetClock) {
57 | this.clock.uninstall();
58 | this.resetClock = false;
59 | }
60 | }
61 |
62 | return returnVal;
63 | };
64 |
65 | fakeServerWithClock.restore = function restore() {
66 | if (this.clock) {
67 | this.clock.uninstall();
68 | }
69 |
70 | return fakeServer.restore.apply(this, arguments);
71 | };
72 |
73 | module.exports = fakeServerWithClock;
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nise",
3 | "version": "6.1.1",
4 | "description": "Fake XHR and server",
5 | "keywords": [
6 | "test",
7 | "testing",
8 | "fake",
9 | "mock",
10 | "xhr",
11 | "server"
12 | ],
13 | "repository": {
14 | "type": "git",
15 | "url": "http://github.com/sinonjs/nise.git"
16 | },
17 | "main": "lib/index.js",
18 | "module": "nise.js",
19 | "scripts": {
20 | "bundle": "browserify -x timers -x timers/promises --no-detect-globals -s nise -o nise.js lib/index.js",
21 | "lint": "eslint --max-warnings 35 .",
22 | "prettier:check": "prettier --check '**/*.{js,css,md}'",
23 | "prettier:write": "prettier --write '**/*.{js,css,md}'",
24 | "prepare": "npm run bundle",
25 | "prepublishOnly": "git pull && mkdocs gh-deploy -r upstream || mkdocs gh-deploy -r origin",
26 | "test": "mocha lib/**/*.test.js",
27 | "test:coverage": "nyc --reporter=lcov --reporter=text --all npm test -- --reporter dot",
28 | "test:headless": "mochify --https-server --plugin [ proxyquire-universal ] --no-detect-globals test/global-hack.js lib/**/*.test.js"
29 | },
30 | "browser": {
31 | "jsdom": false,
32 | "jsdom-global": false
33 | },
34 | "author": "",
35 | "license": "BSD-3-Clause",
36 | "nyc": {
37 | "exclude": [
38 | "nise.js",
39 | "coverage/**",
40 | "**/*.test.js"
41 | ]
42 | },
43 | "files": [
44 | "nise.js",
45 | "lib/**/*.js",
46 | "!lib/**/*.test.js"
47 | ],
48 | "devDependencies": {
49 | "@sinonjs/eslint-config": "^5.0.3",
50 | "@sinonjs/referee": "^11.0.1",
51 | "browserify": "^16.5.2",
52 | "husky": "^4.3.8",
53 | "jsdom": "^25",
54 | "jsdom-global": "3.0.2",
55 | "lint-staged": "^15.2.10",
56 | "mocha": "^10.7.3",
57 | "mochify": "^9.2.0",
58 | "nyc": "^17.0.0",
59 | "prettier": "^3.3.3",
60 | "proxyquire": "^2.1.3",
61 | "proxyquire-universal": "^3.0.1",
62 | "proxyquireify": "^3.2.1",
63 | "sinon": ">=18"
64 | },
65 | "dependencies": {
66 | "@sinonjs/commons": "^3.0.1",
67 | "@sinonjs/fake-timers": "^13.0.1",
68 | "@sinonjs/text-encoding": "^0.7.3",
69 | "just-extend": "^6.2.0",
70 | "path-to-regexp": "^8.1.0"
71 | },
72 | "lint-staged": {
73 | "*.{js,css,md}": "prettier --check",
74 | "*.js": "eslint --quiet"
75 | },
76 | "husky": {
77 | "hooks": {
78 | "pre-commit": "lint-staged"
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Node CI
2 |
3 | on: [push]
4 |
5 | permissions:
6 | contents: read
7 |
8 | jobs:
9 | prettier:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 | - uses: actions/setup-node@v4
14 | with:
15 | node-version: "22"
16 | - name: Cache modules
17 | uses: actions/cache@v4
18 | with:
19 | path: ~/.npm
20 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
21 | restore-keys: |
22 | ${{ runner.os }}-node-
23 | - name: Install dependencies
24 | run: |
25 | npm ci
26 | env:
27 | HUSKY_SKIP_INSTALL: 1
28 | - name: Prettier
29 | run: |
30 | npm run prettier:check
31 |
32 | lint:
33 | runs-on: ubuntu-latest
34 | steps:
35 | - uses: actions/checkout@v4
36 | - uses: actions/setup-node@v4
37 | with:
38 | node-version: "22"
39 | - name: Cache modules
40 | uses: actions/cache@v4
41 | with:
42 | path: ~/.npm
43 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
44 | restore-keys: |
45 | ${{ runner.os }}-node-
46 | - name: Install dependencies
47 | run: |
48 | npm ci
49 | env:
50 | HUSKY_SKIP_INSTALL: 1
51 | - name: ESLint
52 | run: |
53 | npm run lint
54 |
55 | coverage:
56 | runs-on: ubuntu-latest
57 | steps:
58 | - uses: actions/checkout@v4
59 | - uses: actions/setup-node@v4
60 | with:
61 | node-version: "20"
62 | - name: Cache npm
63 | uses: actions/cache@v4
64 | with:
65 | path: ~/.npm
66 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
67 | restore-keys: |
68 | ${{ runner.os }}-node-
69 | - name: Install dependencies
70 | run: |
71 | npm ci
72 | env:
73 | HUSKY_SKIP_INSTALL: 1
74 | - name: Integration
75 | run: |
76 | npm run test:coverage
77 | - name: Upload coverage report
78 | run: bash <(curl -s https://codecov.io/bash) -F unit -s coverage/lcov.info
79 |
80 | test:
81 | runs-on: ubuntu-latest
82 |
83 | strategy:
84 | matrix:
85 | node-version: [16]
86 |
87 | steps:
88 | - uses: actions/checkout@v4
89 | - name: Use Node.js ${{ matrix.node-version }}
90 | uses: actions/setup-node@v4
91 | with:
92 | node-version: ${{ matrix.node-version }}
93 | - name: Cache modules
94 | uses: actions/cache@v4
95 | with:
96 | path: ~/.npm
97 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
98 | restore-keys: |
99 | ${{ runner.os }}-node-
100 | - name: Install dependencies
101 | run: |
102 | npm ci
103 | env:
104 | HUSKY_SKIP_INSTALL: 1
105 | - name: npm test
106 | run: npm test
107 |
--------------------------------------------------------------------------------
/lib/event/event-target.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | function flattenOptions(options) {
4 | if (options !== Object(options)) {
5 | return {
6 | capture: Boolean(options),
7 | once: false,
8 | passive: false,
9 | };
10 | }
11 | return {
12 | capture: Boolean(options.capture),
13 | once: Boolean(options.once),
14 | passive: Boolean(options.passive),
15 | };
16 | }
17 | function not(fn) {
18 | return function () {
19 | return !fn.apply(this, arguments);
20 | };
21 | }
22 | function hasListenerFilter(listener, capture) {
23 | return function (listenerSpec) {
24 | return (
25 | listenerSpec.capture === capture &&
26 | listenerSpec.listener === listener
27 | );
28 | };
29 | }
30 |
31 | var EventTarget = {
32 | // https://dom.spec.whatwg.org/#dom-eventtarget-addeventlistener
33 | addEventListener: function addEventListener(
34 | event,
35 | listener,
36 | providedOptions,
37 | ) {
38 | // 3. Let capture, passive, and once be the result of flattening more options.
39 | // Flatten property before executing step 2,
40 | // feture detection is usually based on registering handler with options object,
41 | // that has getter defined
42 | // addEventListener("load", () => {}, {
43 | // get once() { supportsOnce = true; }
44 | // });
45 | var options = flattenOptions(providedOptions);
46 |
47 | // 2. If callback is null, then return.
48 | if (listener === null || listener === undefined) {
49 | return;
50 | }
51 |
52 | this.eventListeners = this.eventListeners || {};
53 | this.eventListeners[event] = this.eventListeners[event] || [];
54 |
55 | // 4. If context object’s associated list of event listener
56 | // does not contain an event listener whose type is type,
57 | // callback is callback, and capture is capture, then append
58 | // a new event listener to it, whose type is type, callback is
59 | // callback, capture is capture, passive is passive, and once is once.
60 | if (
61 | !this.eventListeners[event].some(
62 | hasListenerFilter(listener, options.capture),
63 | )
64 | ) {
65 | this.eventListeners[event].push({
66 | listener: listener,
67 | capture: options.capture,
68 | once: options.once,
69 | });
70 | }
71 | },
72 |
73 | // https://dom.spec.whatwg.org/#dom-eventtarget-removeeventlistener
74 | removeEventListener: function removeEventListener(
75 | event,
76 | listener,
77 | providedOptions,
78 | ) {
79 | if (!this.eventListeners || !this.eventListeners[event]) {
80 | return;
81 | }
82 |
83 | // 2. Let capture be the result of flattening options.
84 | var options = flattenOptions(providedOptions);
85 |
86 | // 3. If there is an event listener in the associated list of
87 | // event listeners whose type is type, callback is callback,
88 | // and capture is capture, then set that event listener’s
89 | // removed to true and remove it from the associated list of event listeners.
90 | this.eventListeners[event] = this.eventListeners[event].filter(
91 | not(hasListenerFilter(listener, options.capture)),
92 | );
93 | },
94 |
95 | dispatchEvent: function dispatchEvent(event) {
96 | if (!this.eventListeners || !this.eventListeners[event.type]) {
97 | return Boolean(event.defaultPrevented);
98 | }
99 |
100 | var self = this;
101 | var type = event.type;
102 | var listeners = self.eventListeners[type];
103 |
104 | // Remove listeners, that should be dispatched once
105 | // before running dispatch loop to avoid nested dispatch issues
106 | self.eventListeners[type] = listeners.filter(function (listenerSpec) {
107 | return !listenerSpec.once;
108 | });
109 | listeners.forEach(function (listenerSpec) {
110 | var listener = listenerSpec.listener;
111 | if (typeof listener === "function") {
112 | listener.call(self, event);
113 | } else {
114 | listener.handleEvent(event);
115 | }
116 | });
117 |
118 | return Boolean(event.defaultPrevented);
119 | },
120 | };
121 |
122 | module.exports = EventTarget;
123 |
--------------------------------------------------------------------------------
/lib/configure-logger/index.test.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var assert = require("@sinonjs/referee").assert;
4 | var refute = require("@sinonjs/referee").refute;
5 | var sinon = require("sinon");
6 |
7 | var configureLogError = require("./index");
8 |
9 | describe("configureLogger", function () {
10 | beforeEach(function () {
11 | this.sandbox = sinon.createSandbox();
12 | this.timeOutStub = this.sandbox.stub();
13 | });
14 |
15 | afterEach(function () {
16 | this.sandbox.restore();
17 | });
18 |
19 | it("is a function", function () {
20 | var instance = configureLogError();
21 | assert.isFunction(instance);
22 | });
23 |
24 | it("calls config.logger function with a String", function () {
25 | var spy = this.sandbox.spy();
26 | var logError = configureLogError({
27 | logger: spy,
28 | setTimeout: this.timeOutStub,
29 | useImmediateExceptions: false,
30 | });
31 | var name = "Quisque consequat, elit id suscipit.";
32 | var message =
33 | "Pellentesque gravida orci in tellus tristique, ac commodo nibh congue.";
34 | var error = new Error();
35 |
36 | error.name = name;
37 | error.message = message;
38 |
39 | logError("a label", error);
40 |
41 | assert(spy.called);
42 | assert(spy.calledWithMatch(name));
43 | assert(spy.calledWithMatch(message));
44 | });
45 |
46 | it("calls config.logger function with a stack", function () {
47 | var spy = this.sandbox.spy();
48 | var logError = configureLogError({
49 | logger: spy,
50 | setTimeout: this.timeOutStub,
51 | useImmediateExceptions: false,
52 | });
53 | var stack =
54 | "Integer rutrum dictum elit, posuere accumsan nisi pretium vel. Phasellus adipiscing.";
55 | var error = new Error();
56 |
57 | error.stack = stack;
58 |
59 | logError("another label", error);
60 |
61 | assert(spy.called);
62 | assert(spy.calledWithMatch(stack));
63 | });
64 |
65 | it("should call config.setTimeout", function () {
66 | var logError = configureLogError({
67 | setTimeout: this.timeOutStub,
68 | useImmediateExceptions: false,
69 | });
70 | var error = new Error();
71 |
72 | logError("some wonky label", error);
73 |
74 | assert(this.timeOutStub.calledOnce);
75 | });
76 |
77 | it("should pass a throwing function to config.setTimeout", function () {
78 | var logError = configureLogError({
79 | setTimeout: this.timeOutStub,
80 | useImmediateExceptions: false,
81 | });
82 | var error = new Error();
83 |
84 | logError("async error", error);
85 |
86 | var func = this.timeOutStub.args[0][0];
87 | assert.exception(func);
88 | });
89 |
90 | describe("config.useImmediateExceptions", function () {
91 | beforeEach(function () {
92 | this.sandbox = sinon.createSandbox();
93 | this.timeOutStub = this.sandbox.stub();
94 | });
95 |
96 | afterEach(function () {
97 | this.sandbox.restore();
98 | });
99 |
100 | it("throws the logged error immediately, does not call logError.setTimeout when flag is true", function () {
101 | var error = new Error();
102 | var logError = configureLogError({
103 | setTimeout: this.timeOutStub,
104 | useImmediateExceptions: true,
105 | });
106 |
107 | assert.exception(function () {
108 | logError("an error", error);
109 | });
110 | assert(this.timeOutStub.notCalled);
111 | });
112 |
113 | it("does not throw logged error immediately and calls logError.setTimeout when flag is false", function () {
114 | var error = new Error();
115 | var logError = configureLogError({
116 | setTimeout: this.timeOutStub,
117 | useImmediateExceptions: false,
118 | });
119 |
120 | refute.exception(function () {
121 | logError("an error", error);
122 | });
123 | assert(this.timeOutStub.called);
124 | });
125 | });
126 |
127 | describe("#835", function () {
128 | it("logError() throws an exception if the passed err is read-only", function () {
129 | var logError = configureLogError({ useImmediateExceptions: true });
130 |
131 | // passes
132 | var err = {
133 | name: "TestError",
134 | message: "this is a proper exception",
135 | };
136 | assert.exception(
137 | function () {
138 | logError("#835 test", err);
139 | },
140 | {
141 | name: err.name,
142 | },
143 | );
144 |
145 | // fails until this issue is fixed
146 | assert.exception(
147 | function () {
148 | logError(
149 | "#835 test",
150 | "this literal string is not a proper exception",
151 | );
152 | },
153 | {
154 | name: "#835 test",
155 | },
156 | );
157 | });
158 | });
159 | });
160 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8 |
9 | ## Our Standards
10 |
11 | Examples of behavior that contributes to a positive environment for our community include:
12 |
13 | - Demonstrating empathy and kindness toward other people
14 | - Being respectful of differing opinions, viewpoints, and experiences
15 | - Giving and gracefully accepting constructive feedback
16 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17 | - Focusing on what is best not just for us as individuals, but for the overall community
18 |
19 | Examples of unacceptable behavior include:
20 |
21 | - The use of sexualized language or imagery, and sexual attention or
22 | advances of any kind
23 | - Trolling, insulting or derogatory comments, and personal or political attacks
24 | - Public or private harassment
25 | - Publishing others' private information, such as a physical or email
26 | address, without their explicit permission
27 | - Other conduct which could reasonably be considered inappropriate in a
28 | professional setting
29 |
30 | ## Enforcement Responsibilities
31 |
32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33 |
34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35 |
36 | ## Scope
37 |
38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39 |
40 | ## Enforcement
41 |
42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement by e-mailing any or all of [Morgan Roderick](mailto:morgan@roderick.dk), [Max Antoni](mailto:mail@maxantoni.de), [Carl-Erik Kopseng](mailto:carlerik@gmail.com). All complaints will be reviewed and investigated promptly and fairly.
43 |
44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45 |
46 | ## Enforcement Guidelines
47 |
48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49 |
50 | ### 1. Correction
51 |
52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53 |
54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55 |
56 | ### 2. Warning
57 |
58 | **Community Impact**: A violation through a single incident or series of actions.
59 |
60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61 |
62 | ### 3. Temporary Ban
63 |
64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65 |
66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67 |
68 | ### 4. Permanent Ban
69 |
70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71 |
72 | **Consequence**: A permanent ban from any sort of public interaction within the community.
73 |
74 | ## Attribution
75 |
76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78 |
79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80 |
81 | [homepage]: https://www.contributor-covenant.org
82 |
83 | For answers to common questions about this code of conduct, see the FAQ at
84 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
85 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # nise (偽)
2 |
3 | [](https://www.npmjs.com/package/nise)
4 | [](https://codecov.io/gh/sinonjs/nise)
5 |
6 |
7 | fake XHR and Server
8 |
9 | Documentation: http://sinonjs.github.io/nise/
10 |
11 | ## Backers
12 |
13 | Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/sinon#backer)]
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | ## Sponsors
47 |
48 | Become a sponsor and get your logo on our README on GitHub with a link to your site. [[Become a sponsor](https://opencollective.com/sinon#sponsor)]
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | ## License
82 |
83 | nise was released under [BSD-3](LICENSE)
84 |
--------------------------------------------------------------------------------
/lib/event/index.test.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var assert = require("@sinonjs/referee").assert;
4 | var extend = require("just-extend");
5 | var sinon = require("sinon");
6 |
7 | var Event = require("./index").Event;
8 | var EventTarget = require("./index").EventTarget;
9 | var ProgressEvent = require("./index").ProgressEvent;
10 | var CustomEvent = require("./index").CustomEvent;
11 |
12 | describe("EventTarget", function () {
13 | beforeEach(function () {
14 | this.target = extend({}, EventTarget);
15 | });
16 |
17 | it("notifies event listener", function () {
18 | var listener = sinon.spy();
19 | this.target.addEventListener("placeHolder", listener);
20 |
21 | var event = new Event("placeHolder");
22 | this.target.dispatchEvent(event);
23 |
24 | assert(listener.calledOnce);
25 | assert(listener.calledWith(event));
26 | });
27 |
28 | it("notifies event listener with target as this", function () {
29 | var listener = sinon.spy();
30 | this.target.addEventListener("placeHolder", listener);
31 |
32 | var event = new Event("placeHolder");
33 | this.target.dispatchEvent(event);
34 |
35 | assert(listener.calledOn(this.target));
36 | });
37 |
38 | it("notifies all event listeners", function () {
39 | var listeners = [sinon.spy(), sinon.spy()];
40 | this.target.addEventListener("placeHolder", listeners[0]);
41 | this.target.addEventListener("placeHolder", listeners[1]);
42 |
43 | var event = new Event("placeHolder");
44 | this.target.dispatchEvent(event);
45 |
46 | assert(listeners[0].calledOnce);
47 | assert(listeners[0].calledOnce);
48 | });
49 |
50 | it("notifies event listener of type listener", function () {
51 | var listener = { handleEvent: sinon.spy() };
52 | this.target.addEventListener("placeHolder", listener);
53 |
54 | this.target.dispatchEvent(new Event("placeHolder"));
55 |
56 | assert(listener.handleEvent.calledOnce);
57 | });
58 |
59 | it("notifies event listener once if added twice without useCapture flag", function () {
60 | var listener = sinon.spy();
61 | this.target.addEventListener("placeHolder", listener);
62 | this.target.addEventListener("placeHolder", listener);
63 |
64 | var event = new Event("placeHolder");
65 | this.target.dispatchEvent(event);
66 |
67 | assert.equals(listener.callCount, 1, "listener only called once");
68 | assert(listener.calledWith(event));
69 | });
70 |
71 | it("notifies event listener twice if added with different capture flag values, ignores other flags", function () {
72 | var listener = sinon.spy();
73 | this.target.addEventListener("placeHolder", listener, {
74 | capture: false,
75 | });
76 | this.target.addEventListener("placeHolder", listener, {
77 | capture: true,
78 | });
79 | this.target.addEventListener("placeHolder", listener, {
80 | capture: true,
81 | once: true,
82 | });
83 | this.target.addEventListener("placeHolder", listener, {
84 | capture: true,
85 | passive: true,
86 | });
87 |
88 | var event = new Event("placeHolder");
89 | this.target.dispatchEvent(event);
90 |
91 | assert.equals(listener.callCount, 2, "listener only called twice");
92 | assert(listener.calledWith(event));
93 | });
94 |
95 | it("uses options of first listener registration", function () {
96 | var listener = sinon.spy();
97 | this.target.addEventListener("placeHolder", listener, {
98 | capture: false,
99 | once: false,
100 | });
101 | // this registration should be ignored
102 | this.target.addEventListener("placeHolder", listener, {
103 | capture: false,
104 | once: true,
105 | });
106 |
107 | var firstEvent = new Event("placeHolder");
108 | this.target.dispatchEvent(firstEvent);
109 |
110 | assert.equals(listener.callCount, 1, "listener only called once");
111 | assert(listener.lastCall.calledWith(sinon.match.same(firstEvent)));
112 |
113 | var secondEvent = new Event("placeHolder");
114 | this.target.dispatchEvent(secondEvent);
115 |
116 | assert.equals(listener.callCount, 2, "listener only called twice");
117 | assert(listener.lastCall.calledWith(sinon.match.same(secondEvent)));
118 | });
119 |
120 | it("feature detection for 'once' flag works", function () {
121 | var onceSupported = false;
122 |
123 | this.target.addEventListener(
124 | "placeHolder",
125 | null,
126 | Object.defineProperty({}, "once", {
127 | get: function () {
128 | onceSupported = true;
129 | return;
130 | },
131 | }),
132 | );
133 |
134 | assert(onceSupported);
135 | });
136 |
137 | it("supports registering event handler with 'once' flag", function () {
138 | var listener = sinon.spy();
139 | this.target.addEventListener("placeHolder", listener, { once: true });
140 |
141 | var firstEvent = new Event("placeHolder");
142 | this.target.dispatchEvent(firstEvent);
143 |
144 | assert.equals(listener.callCount, 1, "listener only called once");
145 | assert(listener.calledWith(sinon.match.same(firstEvent)));
146 |
147 | var secondEvent = new Event("placeHolder");
148 | this.target.dispatchEvent(secondEvent);
149 |
150 | assert.equals(
151 | listener.callCount,
152 | 1,
153 | "listener was not called second time",
154 | );
155 | assert(!listener.calledWith(sinon.match.same(secondEvent)));
156 | });
157 |
158 | it("supports re-registering event handler with 'once' flag after dispatch", function () {
159 | var listener = sinon.spy();
160 | this.target.addEventListener("placeHolder", listener, { once: true });
161 |
162 | var firstEvent = new Event("placeHolder");
163 | this.target.dispatchEvent(firstEvent);
164 |
165 | assert.equals(listener.callCount, 1, "listener only called once");
166 | assert(listener.calledWith(sinon.match.same(firstEvent)));
167 |
168 | var secondEvent = new Event("placeHolder");
169 | this.target.dispatchEvent(secondEvent);
170 |
171 | this.target.addEventListener("placeHolder", listener, { once: true });
172 |
173 | var thirdEvent = new Event("placeHolder");
174 | this.target.dispatchEvent(thirdEvent);
175 |
176 | assert.equals(
177 | listener.callCount,
178 | 2,
179 | "listener called second time after re-registration",
180 | );
181 | assert(listener.calledWith(sinon.match.same(thirdEvent)));
182 | });
183 |
184 | it("does not notify listeners of other events", function () {
185 | var listeners = [sinon.spy(), sinon.spy()];
186 | this.target.addEventListener("placeHolder", listeners[0]);
187 | this.target.addEventListener("other", listeners[1]);
188 |
189 | this.target.dispatchEvent(new Event("placeHolder"));
190 |
191 | assert.isFalse(listeners[1].called);
192 | });
193 |
194 | it("does not notify unregistered listeners", function () {
195 | var listener = sinon.spy();
196 | this.target.addEventListener("placeHolder", listener);
197 | this.target.removeEventListener("placeHolder", listener);
198 |
199 | this.target.dispatchEvent(new Event("placeHolder"));
200 |
201 | assert.isFalse(listener.called);
202 | });
203 |
204 | it("notifies existing listeners after removing one", function () {
205 | var listeners = [sinon.spy(), sinon.spy(), sinon.spy()];
206 | this.target.addEventListener("placeHolder", listeners[0]);
207 | this.target.addEventListener("placeHolder", listeners[1]);
208 | this.target.addEventListener("placeHolder", listeners[2]);
209 | this.target.removeEventListener("placeHolder", listeners[1]);
210 |
211 | this.target.dispatchEvent(new Event("placeHolder"));
212 |
213 | assert(listeners[0].calledOnce);
214 | assert(listeners[2].calledOnce);
215 | });
216 |
217 | it("returns false when event.preventDefault is not called", function () {
218 | this.target.addEventListener("placeHolder", sinon.spy());
219 |
220 | var event = new Event("placeHolder");
221 | var result = this.target.dispatchEvent(event);
222 |
223 | assert.isFalse(result);
224 | });
225 |
226 | it("returns true when event.preventDefault is called", function () {
227 | this.target.addEventListener("placeHolder", function (e) {
228 | e.preventDefault();
229 | });
230 |
231 | var result = this.target.dispatchEvent(new Event("placeHolder"));
232 |
233 | assert.isTrue(result);
234 | });
235 |
236 | it("notifies ProgressEvent listener with progress data ", function () {
237 | var listener = sinon.spy();
238 | this.target.addEventListener("placeHolderProgress", listener);
239 |
240 | var progressEvent = new ProgressEvent("placeHolderProgress", {
241 | loaded: 50,
242 | total: 120,
243 | });
244 | this.target.dispatchEvent(progressEvent);
245 |
246 | assert.isTrue(progressEvent.lengthComputable);
247 | assert(listener.calledOnce);
248 | assert(listener.calledWith(progressEvent));
249 | });
250 |
251 | it("notifies CustomEvent listener with custom data", function () {
252 | var listener = sinon.spy();
253 | this.target.addEventListener("placeHolderCustom", listener);
254 |
255 | var customEvent = new CustomEvent("placeHolderCustom", {
256 | detail: "hola",
257 | });
258 | this.target.dispatchEvent(customEvent);
259 |
260 | assert(listener.calledOnce);
261 | assert(listener.calledWith(customEvent));
262 | });
263 | });
264 |
--------------------------------------------------------------------------------
/lib/fake-server/fake-server-with-clock.test.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var JSDOM = require("jsdom").JSDOM;
4 | var referee = require("@sinonjs/referee");
5 | var setupDOM = require("jsdom-global");
6 | var sinon = require("sinon");
7 |
8 | var fakeServerWithClock = require("./fake-server-with-clock");
9 | var sinonFakeServer = require("./index");
10 |
11 | var FakeTimers = require("@sinonjs/fake-timers");
12 | var FakeXMLHttpRequest = require("../fake-xhr").FakeXMLHttpRequest;
13 |
14 | var JSDOMParser;
15 | if (JSDOM) {
16 | JSDOMParser = new JSDOM().window.DOMParser;
17 | }
18 |
19 | var assert = referee.assert;
20 | var refute = referee.refute;
21 |
22 | var globalSetTimeout = setTimeout;
23 |
24 | describe("fakeServerWithClock", function () {
25 | beforeEach(function () {
26 | if (JSDOMParser) {
27 | global.DOMParser = JSDOMParser;
28 | this.cleanupDOM = setupDOM();
29 | }
30 | });
31 |
32 | afterEach(function () {
33 | if (JSDOMParser) {
34 | delete global.DOMParser;
35 | this.cleanupDOM();
36 | }
37 | });
38 |
39 | describe("without pre-existing fake clock", function () {
40 | beforeEach(function () {
41 | this.server = fakeServerWithClock.create();
42 | });
43 |
44 | afterEach(function () {
45 | this.server.restore();
46 | if (this.clock) {
47 | this.clock.uninstall();
48 | }
49 | });
50 |
51 | it("calls 'super' when adding requests", function () {
52 | var sandbox = sinon.createSandbox();
53 | var addRequest = sandbox.stub(sinonFakeServer, "addRequest");
54 | var xhr = {};
55 | this.server.addRequest(xhr);
56 |
57 | assert(addRequest.calledWith(xhr));
58 | assert(addRequest.calledOn(this.server));
59 | sandbox.restore();
60 | });
61 |
62 | it("sets reference to clock when adding async request", function () {
63 | this.server.addRequest({ async: true });
64 |
65 | assert.isObject(this.server.clock);
66 | assert.isFunction(this.server.clock.tick);
67 | });
68 |
69 | it("sets longest timeout from setTimeout", function () {
70 | this.server.addRequest({ async: true });
71 |
72 | // eslint-disable-next-line no-empty-function
73 | setTimeout(function () {}, 12);
74 | // eslint-disable-next-line no-empty-function
75 | setTimeout(function () {}, 29);
76 | // eslint-disable-next-line no-empty-function
77 | setInterval(function () {}, 12);
78 | // eslint-disable-next-line no-empty-function
79 | setTimeout(function () {}, 27);
80 |
81 | assert.equals(this.server.longestTimeout, 29);
82 | });
83 |
84 | it("sets longest timeout from setInterval", function () {
85 | this.server.addRequest({ async: true });
86 |
87 | // eslint-disable-next-line no-empty-function
88 | setTimeout(function () {}, 12);
89 | // eslint-disable-next-line no-empty-function
90 | setTimeout(function () {}, 29);
91 | // eslint-disable-next-line no-empty-function
92 | setInterval(function () {}, 132);
93 | // eslint-disable-next-line no-empty-function
94 | setTimeout(function () {}, 27);
95 |
96 | assert.equals(this.server.longestTimeout, 132);
97 | });
98 |
99 | it("resets clock", function () {
100 | this.server.addRequest({ async: true });
101 |
102 | this.server.respond("");
103 | assert.same(setTimeout, globalSetTimeout);
104 | });
105 |
106 | it("does not reset clock second time", function () {
107 | this.server.addRequest({ async: true });
108 | this.server.respond("");
109 | this.clock = FakeTimers.install();
110 | this.server.addRequest({ async: true });
111 | this.server.respond("");
112 |
113 | refute.same(setTimeout, globalSetTimeout);
114 | });
115 | });
116 |
117 | describe("existing clock", function () {
118 | beforeEach(function () {
119 | this.clock = FakeTimers.install();
120 | this.server = fakeServerWithClock.create();
121 | });
122 |
123 | afterEach(function () {
124 | this.clock.uninstall();
125 | this.server.restore();
126 | });
127 |
128 | it("uses existing clock", function () {
129 | this.server.addRequest({ async: true });
130 |
131 | assert.same(this.server.clock, this.clock);
132 | });
133 |
134 | it("records longest timeout using setTimeout and existing clock", function () {
135 | this.server.addRequest({ async: true });
136 |
137 | // eslint-disable-next-line no-empty-function
138 | setInterval(function () {}, 42);
139 | // eslint-disable-next-line no-empty-function
140 | setTimeout(function () {}, 23);
141 | // eslint-disable-next-line no-empty-function
142 | setTimeout(function () {}, 53);
143 | // eslint-disable-next-line no-empty-function
144 | setInterval(function () {}, 12);
145 |
146 | assert.same(this.server.longestTimeout, 53);
147 | });
148 |
149 | it("records longest timeout using setInterval and existing clock", function () {
150 | this.server.addRequest({ async: true });
151 |
152 | // eslint-disable-next-line no-empty-function
153 | setInterval(function () {}, 92);
154 | // eslint-disable-next-line no-empty-function
155 | setTimeout(function () {}, 73);
156 | // eslint-disable-next-line no-empty-function
157 | setTimeout(function () {}, 53);
158 | // eslint-disable-next-line no-empty-function
159 | setInterval(function () {}, 12);
160 |
161 | assert.same(this.server.longestTimeout, 92);
162 | });
163 |
164 | it("does not reset clock", function () {
165 | this.server.respond("");
166 |
167 | assert.same(setTimeout.clock, this.clock);
168 | });
169 | });
170 |
171 | describe(".respond", function () {
172 | var sandbox;
173 |
174 | beforeEach(function () {
175 | this.server = fakeServerWithClock.create();
176 | this.server.addRequest({ async: true });
177 | });
178 |
179 | afterEach(function () {
180 | this.server.restore();
181 | if (sandbox) {
182 | sandbox.restore();
183 | sandbox = null;
184 | }
185 | });
186 |
187 | it("ticks the clock to fire the longest timeout", function () {
188 | this.server.longestTimeout = 96;
189 |
190 | this.server.respond();
191 |
192 | assert.equals(this.server.clock.now, 96);
193 | });
194 |
195 | it("ticks the clock to fire the longest timeout when multiple responds", function () {
196 | // eslint-disable-next-line no-empty-function
197 | setInterval(function () {}, 13);
198 | this.server.respond();
199 | var xhr = new FakeXMLHttpRequest();
200 | // please the linter, we can't have unused variables
201 | // even when we're instantiating FakeXMLHttpRequest for its side effects
202 | assert(xhr);
203 | // eslint-disable-next-line no-empty-function
204 | setInterval(function () {}, 17);
205 | this.server.respond();
206 |
207 | assert.equals(this.server.clock.now, 17);
208 | });
209 |
210 | it("resets longest timeout", function () {
211 | this.server.longestTimeout = 96;
212 |
213 | this.server.respond();
214 |
215 | assert.equals(this.server.longestTimeout, 0);
216 | });
217 |
218 | it("calls original respond", function () {
219 | sandbox = sinon.createSandbox();
220 | var obj = {};
221 | var respond = sandbox.stub(sinonFakeServer, "respond").returns(obj);
222 |
223 | var result = this.server.respond("GET", "/", "");
224 |
225 | assert.equals(result, obj);
226 | assert(respond.calledWith("GET", "/", ""));
227 | assert(respond.calledOn(this.server));
228 | });
229 |
230 | it("does not trigger a timeout event", function () {
231 | sandbox = sinon.createSandbox();
232 |
233 | var xhr = new FakeXMLHttpRequest();
234 | xhr.open("GET", "/");
235 | xhr.timeout = 1;
236 | xhr.triggerTimeout = sandbox.spy();
237 | xhr.send();
238 |
239 | this.server.respond();
240 |
241 | assert.isFalse(xhr.triggerTimeout.called);
242 | });
243 | });
244 |
245 | describe("jQuery compat mode", function () {
246 | beforeEach(function () {
247 | this.server = fakeServerWithClock.create();
248 |
249 | this.request = new FakeXMLHttpRequest();
250 | this.request.open("get", "/", true);
251 | this.request.send();
252 | sinon.spy(this.request, "respond");
253 | });
254 |
255 | afterEach(function () {
256 | this.server.restore();
257 | });
258 |
259 | it("handles clock automatically", function () {
260 | this.server.respondWith("OK");
261 | var spy = sinon.spy();
262 |
263 | setTimeout(spy, 13);
264 | this.server.respond();
265 | this.server.restore();
266 |
267 | assert(spy.called);
268 | assert.same(setTimeout, globalSetTimeout);
269 | });
270 |
271 | it("finishes xhr from setInterval like jQuery 1.3.x does", function () {
272 | this.server.respondWith("Hello World");
273 | var xhr = new FakeXMLHttpRequest();
274 | xhr.open("GET", "/");
275 | xhr.send();
276 |
277 | var spy = sinon.spy();
278 |
279 | setInterval(function () {
280 | spy(xhr.responseText, xhr.statusText, xhr);
281 | }, 13);
282 |
283 | this.server.respond();
284 |
285 | assert.equals(spy.args[0][0], "Hello World");
286 | assert.equals(spy.args[0][1], "OK");
287 | assert.equals(spy.args[0][2].status, 200);
288 | });
289 | });
290 | });
291 |
--------------------------------------------------------------------------------
/lib/fake-server/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var fakeXhr = require("../fake-xhr");
4 | var push = [].push;
5 | var log = require("./log");
6 | var configureLogError = require("../configure-logger");
7 | var pathToRegexp = require("path-to-regexp").pathToRegexp;
8 |
9 | var supportsArrayBuffer = typeof ArrayBuffer !== "undefined";
10 |
11 | function responseArray(handler) {
12 | var response = handler;
13 |
14 | if (Object.prototype.toString.call(handler) !== "[object Array]") {
15 | response = [200, {}, handler];
16 | }
17 |
18 | if (typeof response[2] !== "string") {
19 | if (!supportsArrayBuffer) {
20 | throw new TypeError(
21 | `Fake server response body should be a string, but was ${typeof response[2]}`,
22 | );
23 | } else if (!(response[2] instanceof ArrayBuffer)) {
24 | throw new TypeError(
25 | `Fake server response body should be a string or ArrayBuffer, but was ${typeof response[2]}`,
26 | );
27 | }
28 | }
29 |
30 | return response;
31 | }
32 |
33 | function getDefaultWindowLocation() {
34 | var winloc = {
35 | hostname: "localhost",
36 | port: process.env.PORT || 80,
37 | protocol: "http:",
38 | };
39 | winloc.host =
40 | winloc.hostname +
41 | (String(winloc.port) === "80" ? "" : `:${winloc.port}`);
42 | return winloc;
43 | }
44 |
45 | function getWindowLocation() {
46 | if (typeof window === "undefined") {
47 | // Fallback
48 | return getDefaultWindowLocation();
49 | }
50 |
51 | if (typeof window.location !== "undefined") {
52 | // Browsers place location on window
53 | return window.location;
54 | }
55 |
56 | if (
57 | typeof window.window !== "undefined" &&
58 | typeof window.window.location !== "undefined"
59 | ) {
60 | // React Native on Android places location on window.window
61 | return window.window.location;
62 | }
63 |
64 | return getDefaultWindowLocation();
65 | }
66 |
67 | function matchOne(response, reqMethod, reqUrl) {
68 | var rmeth = response.method;
69 | var matchMethod = !rmeth || rmeth.toLowerCase() === reqMethod.toLowerCase();
70 | var url = response.url;
71 | var matchUrl =
72 | !url ||
73 | url === reqUrl ||
74 | (typeof url.test === "function" && url.test(reqUrl)) ||
75 | (typeof url === "function" && url(reqUrl) === true);
76 |
77 | return matchMethod && matchUrl;
78 | }
79 |
80 | function match(response, request) {
81 | var wloc = getWindowLocation();
82 |
83 | var rCurrLoc = new RegExp(`^${wloc.protocol}//${wloc.host}/`);
84 |
85 | var requestUrl = request.url;
86 |
87 | if (!/^https?:\/\//.test(requestUrl) || rCurrLoc.test(requestUrl)) {
88 | requestUrl = requestUrl.replace(rCurrLoc, "/");
89 | }
90 |
91 | if (matchOne(response, this.getHTTPMethod(request), requestUrl)) {
92 | if (typeof response.response === "function") {
93 | var ru = response.url;
94 | var args = [request].concat(
95 | ru && typeof ru.exec === "function"
96 | ? ru.exec(requestUrl).slice(1)
97 | : [],
98 | );
99 | return response.response.apply(response, args);
100 | }
101 |
102 | return true;
103 | }
104 |
105 | return false;
106 | }
107 |
108 | function incrementRequestCount() {
109 | var count = ++this.requestCount;
110 |
111 | this.requested = true;
112 |
113 | this.requestedOnce = count === 1;
114 | this.requestedTwice = count === 2;
115 | this.requestedThrice = count === 3;
116 |
117 | this.firstRequest = this.getRequest(0);
118 | this.secondRequest = this.getRequest(1);
119 | this.thirdRequest = this.getRequest(2);
120 |
121 | this.lastRequest = this.getRequest(count - 1);
122 | }
123 |
124 | var fakeServer = {
125 | create: function (config) {
126 | var server = Object.create(this);
127 | server.configure(config);
128 | this.xhr = fakeXhr.useFakeXMLHttpRequest();
129 | server.requests = [];
130 | server.requestCount = 0;
131 | server.queue = [];
132 | server.responses = [];
133 |
134 | this.xhr.onCreate = function (xhrObj) {
135 | xhrObj.unsafeHeadersEnabled = function () {
136 | return !(server.unsafeHeadersEnabled === false);
137 | };
138 | server.addRequest(xhrObj);
139 | };
140 |
141 | return server;
142 | },
143 |
144 | configure: function (config) {
145 | var self = this;
146 | var allowlist = {
147 | autoRespond: true,
148 | autoRespondAfter: true,
149 | respondImmediately: true,
150 | fakeHTTPMethods: true,
151 | logger: true,
152 | unsafeHeadersEnabled: true,
153 | };
154 |
155 | // eslint-disable-next-line no-param-reassign
156 | config = config || {};
157 |
158 | Object.keys(config).forEach(function (setting) {
159 | if (setting in allowlist) {
160 | self[setting] = config[setting];
161 | }
162 | });
163 |
164 | self.logError = configureLogError(config);
165 | },
166 |
167 | addRequest: function addRequest(xhrObj) {
168 | var server = this;
169 | push.call(this.requests, xhrObj);
170 |
171 | incrementRequestCount.call(this);
172 |
173 | xhrObj.onSend = function () {
174 | server.handleRequest(this);
175 |
176 | if (server.respondImmediately) {
177 | server.respond();
178 | } else if (server.autoRespond && !server.responding) {
179 | setTimeout(function () {
180 | server.responding = false;
181 | server.respond();
182 | }, server.autoRespondAfter || 10);
183 |
184 | server.responding = true;
185 | }
186 | };
187 | },
188 |
189 | getHTTPMethod: function getHTTPMethod(request) {
190 | if (this.fakeHTTPMethods && /post/i.test(request.method)) {
191 | var matches = (request.requestBody || "").match(
192 | /_method=([^\b;]+)/,
193 | );
194 | return matches ? matches[1] : request.method;
195 | }
196 |
197 | return request.method;
198 | },
199 |
200 | handleRequest: function handleRequest(xhr) {
201 | if (xhr.async) {
202 | push.call(this.queue, xhr);
203 | } else {
204 | this.processRequest(xhr);
205 | }
206 | },
207 |
208 | logger: function () {
209 | // no-op; override via configure()
210 | },
211 |
212 | logError: configureLogError({}),
213 |
214 | log: log,
215 |
216 | respondWith: function respondWith(method, url, body) {
217 | if (arguments.length === 1 && typeof method !== "function") {
218 | this.response = responseArray(method);
219 | return;
220 | }
221 |
222 | if (arguments.length === 1) {
223 | // eslint-disable-next-line no-param-reassign
224 | body = method;
225 | // eslint-disable-next-line no-param-reassign
226 | url = method = null;
227 | }
228 |
229 | if (arguments.length === 2) {
230 | // eslint-disable-next-line no-param-reassign
231 | body = url;
232 | // eslint-disable-next-line no-param-reassign
233 | url = method;
234 | // eslint-disable-next-line no-param-reassign
235 | method = null;
236 | }
237 |
238 | // Escape port number to prevent "named" parameters in 'path-to-regexp' module
239 | if (typeof url === "string" && url !== "") {
240 | if (/:[0-9]+\//.test(url)) {
241 | var m = url.match(/^(https?:\/\/.*?):([0-9]+\/.*)$/);
242 | // eslint-disable-next-line no-param-reassign
243 | url = `${m[1]}\\:${m[2]}`;
244 | }
245 | if (/:\/\//.test(url)) {
246 | // eslint-disable-next-line no-param-reassign
247 | url = url.replace("://", "\\://");
248 | }
249 | if (/\*/.test(url)) {
250 | // Uses the new syntax for repeating parameters in path-to-regexp,
251 | // see https://github.com/pillarjs/path-to-regexp#unexpected--or-
252 | // eslint-disable-next-line no-param-reassign
253 | url = url.replace(/\/\*/g, "/*path");
254 | }
255 | }
256 | push.call(this.responses, {
257 | method: method,
258 | url:
259 | typeof url === "string" && url !== ""
260 | ? pathToRegexp(url).regexp
261 | : url,
262 | response: typeof body === "function" ? body : responseArray(body),
263 | });
264 | },
265 |
266 | respond: function respond() {
267 | if (arguments.length > 0) {
268 | this.respondWith.apply(this, arguments);
269 | }
270 |
271 | var queue = this.queue || [];
272 | var requests = queue.splice(0, queue.length);
273 | var self = this;
274 |
275 | requests.forEach(function (request) {
276 | self.processRequest(request);
277 | });
278 | },
279 |
280 | respondAll: function respondAll() {
281 | if (this.respondImmediately) {
282 | return;
283 | }
284 |
285 | this.queue = this.requests.slice(0);
286 |
287 | var request;
288 | while ((request = this.queue.shift())) {
289 | this.processRequest(request);
290 | }
291 | },
292 |
293 | processRequest: function processRequest(request) {
294 | try {
295 | if (request.aborted) {
296 | return;
297 | }
298 |
299 | var response = this.response || [404, {}, ""];
300 |
301 | if (this.responses) {
302 | for (var l = this.responses.length, i = l - 1; i >= 0; i--) {
303 | if (match.call(this, this.responses[i], request)) {
304 | response = this.responses[i].response;
305 | break;
306 | }
307 | }
308 | }
309 |
310 | if (request.readyState !== 4) {
311 | this.log(response, request);
312 |
313 | request.respond(response[0], response[1], response[2]);
314 | }
315 | } catch (e) {
316 | this.logError("Fake server request processing", e);
317 | }
318 | },
319 |
320 | restore: function restore() {
321 | return this.xhr.restore && this.xhr.restore.apply(this.xhr, arguments);
322 | },
323 |
324 | getRequest: function getRequest(index) {
325 | return this.requests[index] || null;
326 | },
327 |
328 | reset: function reset() {
329 | this.resetBehavior();
330 | this.resetHistory();
331 | },
332 |
333 | resetBehavior: function resetBehavior() {
334 | this.responses.length = this.queue.length = 0;
335 | },
336 |
337 | resetHistory: function resetHistory() {
338 | this.requests.length = this.requestCount = 0;
339 |
340 | this.requestedOnce =
341 | this.requestedTwice =
342 | this.requestedThrice =
343 | this.requested =
344 | false;
345 |
346 | this.firstRequest =
347 | this.secondRequest =
348 | this.thirdRequest =
349 | this.lastRequest =
350 | null;
351 | },
352 | };
353 |
354 | module.exports = fakeServer;
355 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # nise (偽)
2 |
3 | fake XHR and Server
4 |
5 | This module has been extracted from [Sinon.JS][sinon] and can be used standalone. Sinon.JS will always be the "full package". However, there are use cases, where fake XHR and fake Server are needed but the rest of Sinon.JS not. That's the scenario of nise.
6 |
7 | ## Fake `XMLHttpRequest`
8 |
9 | Provides a fake implementation of `XMLHttpRequest` and provides
10 | several interfaces for manipulating objects created by it.
11 |
12 | Also fakes native `XMLHttpRequest` and `ActiveXObject` (when available, and only for `XMLHTTP` progids). Helps with testing requests made with `XHR`.
13 |
14 | ```js
15 | var fakeXhr = require("nise").fakeXhr;
16 | var sinon = require("sinon");
17 |
18 | {
19 | setUp: function () {
20 | this.xhr = fakeXhr.useFakeXMLHttpRequest();
21 | var requests = this.requests = [];
22 |
23 | this.xhr.onCreate = function (xhr) {
24 | requests.push(xhr);
25 | };
26 | },
27 |
28 | tearDown: function () {
29 | this.xhr.restore();
30 | },
31 |
32 | "test should fetch comments from server" : function () {
33 | var callback = sinon.spy();
34 | myLib.getCommentsFor("/some/article", callback);
35 | assertEquals(1, this.requests.length);
36 |
37 | this.requests[0].respond(200, { "Content-Type": "application/json" },
38 | '[{ "id": 12, "comment": "Hey there" }]');
39 | assert(callback.calledWith([{ id: 12, comment: "Hey there" }]));
40 | }
41 | }
42 | ```
43 |
44 | ### `useFakeXMLHttpRequest`
45 |
46 | #### var xhr = fakeXhr.useFakeXMLHttpRequest();
47 |
48 | Causes fakeXhr to replace the native `XMLHttpRequest` object in browsers that support it with a custom implementation which does not send actual requests.
49 |
50 | In browsers that support `ActiveXObject`, this constructor is replaced, and fake objects are returned for `XMLHTTP` progIds. Other progIds, such as `XMLDOM` are left untouched.
51 |
52 | The native `XMLHttpRequest` object will be available at `fakeXhr.xhr.XMLHttpRequest`
53 |
54 | #### `xhr.onCreate = function (xhr) {};`
55 |
56 | By assigning a function to the `onCreate` property of the returned object from `useFakeXMLHttpRequest()` you can subscribe to newly created `FakeXMLHttpRequest` objects. See below for the fake xhr object API.
57 |
58 | Using this observer means you can still reach objects created by e.g. `jQuery.ajax` (or other abstractions/frameworks).
59 |
60 | #### `xhr.restore();`
61 |
62 | Restore original function(s).
63 |
64 | ### `FakeXMLHttpRequest`
65 |
66 | #### `String request.url`
67 |
68 | The URL set on the request object.
69 |
70 | #### `String request.method`
71 |
72 | The request method as a string.
73 |
74 | #### `Object request.requestHeaders`
75 |
76 | An object of all request headers, i.e.:
77 |
78 | ```js
79 | {
80 | "Accept": "text/html, */*",
81 | "Connection": "keep-alive"
82 | }
83 | ```
84 |
85 | #### `String request.requestBody`
86 |
87 | The request body
88 |
89 | #### `int request.status`
90 |
91 | The request's status code.
92 |
93 | `undefined` if the request has not been handled (see [`respond`](#respond) below)
94 |
95 | #### `String request.statusText`
96 |
97 | Only populated if the [`respond`](#respond) method is called (see below).
98 |
99 | #### `boolean request.async`
100 |
101 | Whether or not the request is asynchronous.
102 |
103 | #### `String request.username`
104 |
105 | Username, if any.
106 |
107 | #### `String request.password`
108 |
109 | Password, if any.
110 |
111 | #### `Document request.responseXML`
112 |
113 | When using [`respond`](#respond), this property is populated with a parsed document if response headers indicate as much (see [the spec](http://www.w3.org/TR/XMLHttpRequest/))
114 |
115 | #### `String request.getResponseHeader(header);`
116 |
117 | The value of the given response header, if the request has been responded to (see [`respond`](#respond)).
118 |
119 | #### `Object request.getAllResponseHeaders();`
120 |
121 | All response headers as an object.
122 |
123 | ### Filtered requests
124 |
125 | When using Sinon.JS for mockups or partial integration/functional testing, you might want to fake some requests, while allowing others to go through to the backend server. With filtered `FakeXMLHttpRequest`s (new in v1.3.0), you can.
126 |
127 | #### `FakeXMLHttpRequest.useFilters`
128 |
129 | Default `false`.
130 |
131 | When set to `true`, Sinon will check added filters if certain requests should be "unfaked"
132 |
133 | #### `FakeXMLHttpRequest.addFilter(fn)`
134 |
135 | Add a filter that will decide whether or not to fake a request.
136 |
137 | The filter will be called when `xhr.open` is called, with the exact same arguments (`method`, `url`, `async`, `username`, `password`). If the filter returns `true`, the request will not be faked.
138 |
139 | ### Simulating server responses
140 |
141 | #### `request.setStatus(status);`
142 |
143 | Sets response status (`status` and `statusText` properties).
144 |
145 | Status should be a number, the status text is looked up from `fakeXhr.FakeXMLHttpRequest.statusCodes`.
146 |
147 | #### `request.setResponseHeaders(object);`
148 |
149 | Sets response headers (e.g. `{ "Content-Type": "text/html", /* ... */ }`, updates the `readyState` property and fires `onreadystatechange`.
150 |
151 | #### `request.setResponseBody(body);`
152 |
153 | Sets the respond body, updates the `readyState` property and fires `onreadystatechange`.
154 |
155 | Additionally, populates `responseXML` with a parsed document if [response headers indicate as much](http://www.w3.org/TR/XMLHttpRequest/).
156 |
157 | #### `request.respond(status, headers, body);`
158 |
159 | Calls the above three methods.
160 |
161 | #### `request.error();`
162 |
163 | Simulates a network error on the request. The `onerror` handler will be called and the `status` will be `0`.
164 |
165 | #### `Boolean request.autoRespond`
166 |
167 | When set to `true`, causes the server to automatically respond to incoming requests after a timeout.
168 |
169 | The default timeout is 10ms but you can control it through the `autoRespondAfter` property.
170 |
171 | Note that this feature is intended to help during mockup development, and is not suitable for use in tests.
172 |
173 | #### `Number request.autoRespondAfter`
174 |
175 | When `autoRespond` is `true`, respond to requests after this number of milliseconds. Default is 10.
176 |
177 | ## Fake server
178 |
179 | High-level API to manipulate `FakeXMLHttpRequest` instances.
180 |
181 | For help with handling JSON-P please refer to our [notes below](#json-p)
182 |
183 | ```js
184 | var fakeServer = require("nise").fakeServer;
185 | var sinon = require("sinon");
186 |
187 | {
188 | setUp: function () {
189 | this.server = fakeServer.create();
190 | },
191 |
192 | tearDown: function () {
193 | this.server.restore();
194 | },
195 |
196 | "test should fetch comments from server" : function () {
197 | this.server.respondWith("GET", "/some/article/comments.json",
198 | [200, { "Content-Type": "application/json" },
199 | '[{ "id": 12, "comment": "Hey there" }]']);
200 |
201 | var callback = sinon.spy();
202 | myLib.getCommentsFor("/some/article", callback);
203 | this.server.respond();
204 |
205 | sinon.assert.calledWith(callback, [{ id: 12, comment: "Hey there" }]);
206 |
207 | assert(server.requests.length > 0)
208 | }
209 | }
210 | ```
211 |
212 | #### `var server = fakeServer.create([config]);`
213 |
214 | Creates a new server.
215 |
216 | This function also calls `useFakeXMLHttpRequest()`.
217 |
218 | `create` accepts optional properties to configure the fake server. See [options](#options) below for configuration parameters.
219 |
220 | #### `var server = fakeServerWithClock.create();`
221 |
222 | Creates a server that also manages [fake timers](https://github.com/sinonjs/fake-timers).
223 |
224 | This is useful when testing `XHR` objects created with e.g. jQuery 1.3.x, which uses a timer to poll the object for completion, rather than the usual `onreadystatechange`.
225 |
226 | #### `server.configure(config);`
227 |
228 | Configures the fake server.
229 |
230 | See [options](#options) below for configuration parameters.
231 |
232 | #### `server.respondWith(response);`
233 |
234 | Causes the server to respond to any request not matched by another response with the provided data. The default catch-all response is `[404, {}, ""]`.
235 |
236 | `response` can be one of three things:
237 |
238 | 1. A `String` or `ArrayBuffer` representing the response body
239 | 2. An `Array` with status, headers and response body, e.g. `[200, { "Content-Type": "text/html", "Content-Length": 2 }, "OK"]`
240 | 3. A `Function`.
241 |
242 | Default status is 200 and default headers are none.
243 |
244 | When the response is a `Function`, it will be passed the request object. You
245 | must manually call [respond](#respond) on it to complete the
246 | request.
247 |
248 | #### `server.respondWith(url, response);`
249 |
250 | Responds to all requests to given URL, e.g. `/posts/1`.
251 |
252 | #### `server.respondWith(method, url, response);`
253 |
254 | Responds to all `method` requests to the given URL with the given response.
255 |
256 | `method` is an HTTP verb.
257 |
258 | #### `server.respondWith(urlRegExp, response);`
259 |
260 | URL may be a regular expression, e.g. `/\\/post\\//\\d+`
261 |
262 | If the response is a `Function`, it will be passed any capture groups from the regular expression along with the XMLHttpRequest object:
263 |
264 | ```js
265 | server.respondWith(/\/todo-items\/(\d+)/, function (xhr, id) {
266 | xhr.respond(
267 | 200,
268 | { "Content-Type": "application/json" },
269 | '[{ "id": ' + id + " }]",
270 | );
271 | });
272 | ```
273 |
274 | #### `server.respondWith(method, urlRegExp, response);`
275 |
276 | Responds to all `method` requests to URLs matching the regular expression.
277 |
278 | #### `server.respond();`
279 |
280 | Causes all queued asynchronous requests to receive a response.
281 |
282 | If none of the responses added through `respondWith` match, the default response is `[404, {}, ""]`.
283 |
284 | Synchronous requests are responded to immediately, so make sure to call `respondWith` upfront.
285 |
286 | If called with arguments, `respondWith` will be called with those arguments before responding to requests.
287 |
288 | #### `server.autoRespond = true;`
289 |
290 | If set, will automatically respond to every request after a timeout.
291 |
292 | The default timeout is 10ms but you can control it through the `autoRespondAfter` property.
293 |
294 | Note that this feature is intended to help during mockup development, and is not suitable for use in tests. For synchronous immediate responses, use `respondImmediately` instead.
295 |
296 | #### `server.autoRespondAfter = ms;`
297 |
298 | Causes the server to automatically respond to incoming requests after a timeout.
299 |
300 | #### `server.respondImmediately = true;`
301 |
302 | If set, the server will respond to every request immediately and synchronously.
303 |
304 | This is ideal for faking the server from within a test without having to call `server.respond()` after each request made in that test.
305 |
306 | As this is synchronous and immediate, this is not suitable for simulating actual network latency in tests or mockups. To simulate network latency with automatic responses, see `server.autoRespond` and `server.autoRespondAfter`.
307 |
308 | #### array `server.requests`
309 |
310 | You can inspect the `server.requests` to verify request ordering, find unmatched requests or check that no requests has been done.
311 | `server.requests` is an array of all the `FakeXMLHttpRequest` objects that have been created.
312 |
313 | #### `Boolean server.fakeHTTPMethods`
314 |
315 | If set to `true`, server will find `_method` parameter in POST body and recognize that as the actual method.
316 |
317 | Supports a pattern common to Ruby on Rails applications. For custom HTTP method faking, override `server.getHTTPMethod(request)`.
318 |
319 | #### `server.getHTTPMethod(request)`
320 |
321 | Used internally to determine the HTTP method used with the provided request.
322 |
323 | By default this method simply returns `request.method`. When `server.fakeHTTPMethods` is true, the method will return the value of the `_method` parameter if the method is "POST".
324 |
325 | This method can be overridden to provide custom behavior.
326 |
327 | #### `server.restore();`
328 |
329 | Restores the native XHR constructor.
330 |
331 | ### Fake server options
332 |
333 | These options are properties on the server object and can be set directly
334 |
335 | ```js
336 | server.autoRespond = true;
337 | ```
338 |
339 | You can also pass options with an object literal to `fakeServer.create` and `.configure`.
340 |
341 | #### `Boolean autoRespond`
342 |
343 | If set, will automatically respond to every request after a timeout.
344 |
345 | The default timeout is 10ms but you can control it through the `autoRespondAfter` property.
346 |
347 | Note that this feature is intended to help during mockup development, and is not suitable for use in tests.
348 |
349 | For synchronous immediate responses, use `respondImmediately` instead.
350 |
351 | #### `Number autoRespondAfter (ms)`
352 |
353 | Causes the server to automatically respond to incoming requests after a timeout.
354 |
355 | #### `Boolean respondImmediately`
356 |
357 | If set, the server will respond to every request immediately and synchronously.
358 |
359 | This is ideal for faking the server from within a test without having to call `server.respond()` after each request made in that test.
360 |
361 | As this is synchronous and immediate, this is not suitable for simulating actual network latency in tests or mockups. To simulate network latency with automatic responses, see `server.autoRespond` and `server.autoRespondAfter`.
362 |
363 | #### `boolean fakeHTTPMethods`
364 |
365 | If set to `true`, server will find `_method` parameter in `POST` body and recognize that as the actual method.
366 |
367 | Supports a pattern common to Ruby on Rails applications.
368 |
369 | For custom HTTP method faking, override `server.getHTTPMethod(request)`
370 |
371 | [sinon]: http://sinonjs.org
372 |
--------------------------------------------------------------------------------
/lib/fake-xhr/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var GlobalTextEncoder =
4 | typeof TextEncoder !== "undefined"
5 | ? TextEncoder
6 | : require("@sinonjs/text-encoding").TextEncoder;
7 | var globalObject = require("@sinonjs/commons").global;
8 | var configureLogError = require("../configure-logger");
9 | var sinonEvent = require("../event");
10 | var extend = require("just-extend");
11 |
12 | var supportsProgress = typeof ProgressEvent !== "undefined";
13 | var supportsCustomEvent = typeof CustomEvent !== "undefined";
14 | var supportsFormData = typeof FormData !== "undefined";
15 | var supportsArrayBuffer = typeof ArrayBuffer !== "undefined";
16 | var supportsBlob = require("./blob").isSupported;
17 |
18 | function getWorkingXHR(globalScope) {
19 | var supportsXHR = typeof globalScope.XMLHttpRequest !== "undefined";
20 | if (supportsXHR) {
21 | return globalScope.XMLHttpRequest;
22 | }
23 |
24 | var supportsActiveX = typeof globalScope.ActiveXObject !== "undefined";
25 | if (supportsActiveX) {
26 | return function () {
27 | return new globalScope.ActiveXObject("MSXML2.XMLHTTP.3.0");
28 | };
29 | }
30 |
31 | return false;
32 | }
33 |
34 | // Ref: https://fetch.spec.whatwg.org/#forbidden-header-name
35 | var unsafeHeaders = {
36 | "Accept-Charset": true,
37 | "Access-Control-Request-Headers": true,
38 | "Access-Control-Request-Method": true,
39 | "Accept-Encoding": true,
40 | Connection: true,
41 | "Content-Length": true,
42 | Cookie: true,
43 | Cookie2: true,
44 | "Content-Transfer-Encoding": true,
45 | Date: true,
46 | DNT: true,
47 | Expect: true,
48 | Host: true,
49 | "Keep-Alive": true,
50 | Origin: true,
51 | Referer: true,
52 | TE: true,
53 | Trailer: true,
54 | "Transfer-Encoding": true,
55 | Upgrade: true,
56 | "User-Agent": true,
57 | Via: true,
58 | };
59 |
60 | function EventTargetHandler() {
61 | var self = this;
62 | var events = [
63 | "loadstart",
64 | "progress",
65 | "abort",
66 | "error",
67 | "load",
68 | "timeout",
69 | "loadend",
70 | ];
71 |
72 | function addEventListener(eventName) {
73 | self.addEventListener(eventName, function (event) {
74 | var listener = self[`on${eventName}`];
75 |
76 | if (listener && typeof listener === "function") {
77 | listener.call(this, event);
78 | }
79 | });
80 | }
81 |
82 | events.forEach(addEventListener);
83 | }
84 |
85 | EventTargetHandler.prototype = sinonEvent.EventTarget;
86 |
87 | function normalizeHeaderValue(value) {
88 | // Ref: https://fetch.spec.whatwg.org/#http-whitespace-bytes
89 | /*eslint no-control-regex: "off"*/
90 | return value.replace(/^[\x09\x0A\x0D\x20]+|[\x09\x0A\x0D\x20]+$/g, "");
91 | }
92 |
93 | function getHeader(headers, header) {
94 | var foundHeader = Object.keys(headers).filter(function (h) {
95 | return h.toLowerCase() === header.toLowerCase();
96 | });
97 |
98 | return foundHeader[0] || null;
99 | }
100 |
101 | function excludeSetCookie2Header(header) {
102 | return !/^Set-Cookie2?$/i.test(header);
103 | }
104 |
105 | function verifyResponseBodyType(body, responseType) {
106 | var error = null;
107 | var isString = typeof body === "string";
108 |
109 | if (responseType === "arraybuffer") {
110 | if (!isString && !(body instanceof ArrayBuffer)) {
111 | error = new Error(
112 | `Attempted to respond to fake XMLHttpRequest with ${body}, which is not a string or ArrayBuffer.`,
113 | );
114 | error.name = "InvalidBodyException";
115 | }
116 | } else if (responseType === "blob") {
117 | if (
118 | !isString &&
119 | !(body instanceof ArrayBuffer) &&
120 | supportsBlob &&
121 | !(body instanceof Blob)
122 | ) {
123 | error = new Error(
124 | `Attempted to respond to fake XMLHttpRequest with ${body}, which is not a string, ArrayBuffer, or Blob.`,
125 | );
126 | error.name = "InvalidBodyException";
127 | }
128 | } else if (!isString) {
129 | error = new Error(
130 | `Attempted to respond to fake XMLHttpRequest with ${body}, which is not a string.`,
131 | );
132 | error.name = "InvalidBodyException";
133 | }
134 |
135 | if (error) {
136 | throw error;
137 | }
138 | }
139 |
140 | function convertToArrayBuffer(body, encoding) {
141 | if (body instanceof ArrayBuffer) {
142 | return body;
143 | }
144 |
145 | return new GlobalTextEncoder(encoding || "utf-8").encode(body).buffer;
146 | }
147 |
148 | function isXmlContentType(contentType) {
149 | return (
150 | !contentType ||
151 | /(text\/xml)|(application\/xml)|(\+xml)/.test(contentType)
152 | );
153 | }
154 |
155 | function clearResponse(xhr) {
156 | if (xhr.responseType === "" || xhr.responseType === "text") {
157 | xhr.response = xhr.responseText = "";
158 | } else {
159 | xhr.response = xhr.responseText = null;
160 | }
161 | xhr.responseXML = null;
162 | }
163 |
164 | function fakeXMLHttpRequestFor(globalScope) {
165 | var isReactNative =
166 | globalScope.navigator &&
167 | globalScope.navigator.product === "ReactNative";
168 | var sinonXhr = { XMLHttpRequest: globalScope.XMLHttpRequest };
169 | sinonXhr.GlobalXMLHttpRequest = globalScope.XMLHttpRequest;
170 | sinonXhr.GlobalActiveXObject = globalScope.ActiveXObject;
171 | sinonXhr.supportsActiveX =
172 | typeof sinonXhr.GlobalActiveXObject !== "undefined";
173 | sinonXhr.supportsXHR = typeof sinonXhr.GlobalXMLHttpRequest !== "undefined";
174 | sinonXhr.workingXHR = getWorkingXHR(globalScope);
175 | sinonXhr.supportsTimeout =
176 | sinonXhr.supportsXHR &&
177 | "timeout" in new sinonXhr.GlobalXMLHttpRequest();
178 | sinonXhr.supportsCORS =
179 | isReactNative ||
180 | (sinonXhr.supportsXHR &&
181 | "withCredentials" in new sinonXhr.GlobalXMLHttpRequest());
182 |
183 | // Note that for FakeXMLHttpRequest to work pre ES5
184 | // we lose some of the alignment with the spec.
185 | // To ensure as close a match as possible,
186 | // set responseType before calling open, send or respond;
187 | function FakeXMLHttpRequest(config) {
188 | EventTargetHandler.call(this);
189 | this.readyState = FakeXMLHttpRequest.UNSENT;
190 | this.requestHeaders = {};
191 | this.requestBody = null;
192 | this.status = 0;
193 | this.statusText = "";
194 | this.upload = new EventTargetHandler();
195 | this.responseType = "";
196 | this.response = "";
197 | this.logError = configureLogError(config);
198 |
199 | if (sinonXhr.supportsTimeout) {
200 | this.timeout = 0;
201 | }
202 |
203 | if (sinonXhr.supportsCORS) {
204 | this.withCredentials = false;
205 | }
206 |
207 | if (typeof FakeXMLHttpRequest.onCreate === "function") {
208 | FakeXMLHttpRequest.onCreate(this);
209 | }
210 | }
211 |
212 | function verifyState(xhr) {
213 | if (xhr.readyState !== FakeXMLHttpRequest.OPENED) {
214 | throw new Error("INVALID_STATE_ERR");
215 | }
216 |
217 | if (xhr.sendFlag) {
218 | throw new Error("INVALID_STATE_ERR");
219 | }
220 | }
221 |
222 | // largest arity in XHR is 5 - XHR#open
223 | var apply = function (obj, method, args) {
224 | switch (args.length) {
225 | case 0:
226 | return obj[method]();
227 | case 1:
228 | return obj[method](args[0]);
229 | case 2:
230 | return obj[method](args[0], args[1]);
231 | case 3:
232 | return obj[method](args[0], args[1], args[2]);
233 | case 4:
234 | return obj[method](args[0], args[1], args[2], args[3]);
235 | case 5:
236 | return obj[method](args[0], args[1], args[2], args[3], args[4]);
237 | default:
238 | throw new Error("Unhandled case");
239 | }
240 | };
241 |
242 | FakeXMLHttpRequest.filters = [];
243 | FakeXMLHttpRequest.addFilter = function addFilter(fn) {
244 | this.filters.push(fn);
245 | };
246 | FakeXMLHttpRequest.defake = function defake(fakeXhr, xhrArgs) {
247 | var xhr = new sinonXhr.workingXHR(); // eslint-disable-line new-cap
248 |
249 | [
250 | "open",
251 | "setRequestHeader",
252 | "abort",
253 | "getResponseHeader",
254 | "getAllResponseHeaders",
255 | "addEventListener",
256 | "overrideMimeType",
257 | "removeEventListener",
258 | ].forEach(function (method) {
259 | fakeXhr[method] = function () {
260 | return apply(xhr, method, arguments);
261 | };
262 | });
263 |
264 | fakeXhr.send = function () {
265 | // Ref: https://xhr.spec.whatwg.org/#the-responsetype-attribute
266 | if (xhr.responseType !== fakeXhr.responseType) {
267 | xhr.responseType = fakeXhr.responseType;
268 | }
269 | return apply(xhr, "send", arguments);
270 | };
271 |
272 | var copyAttrs = function (args) {
273 | args.forEach(function (attr) {
274 | fakeXhr[attr] = xhr[attr];
275 | });
276 | };
277 |
278 | var stateChangeStart = function () {
279 | fakeXhr.readyState = xhr.readyState;
280 | if (xhr.readyState >= FakeXMLHttpRequest.HEADERS_RECEIVED) {
281 | copyAttrs(["status", "statusText"]);
282 | }
283 | if (xhr.readyState >= FakeXMLHttpRequest.LOADING) {
284 | copyAttrs(["response"]);
285 | if (xhr.responseType === "" || xhr.responseType === "text") {
286 | copyAttrs(["responseText"]);
287 | }
288 | }
289 | if (
290 | xhr.readyState === FakeXMLHttpRequest.DONE &&
291 | (xhr.responseType === "" || xhr.responseType === "document")
292 | ) {
293 | copyAttrs(["responseXML"]);
294 | }
295 | };
296 |
297 | var stateChangeEnd = function () {
298 | if (fakeXhr.onreadystatechange) {
299 | // eslint-disable-next-line no-useless-call
300 | fakeXhr.onreadystatechange.call(fakeXhr, {
301 | target: fakeXhr,
302 | currentTarget: fakeXhr,
303 | });
304 | }
305 | };
306 |
307 | var stateChange = function stateChange() {
308 | stateChangeStart();
309 | stateChangeEnd();
310 | };
311 |
312 | if (xhr.addEventListener) {
313 | xhr.addEventListener("readystatechange", stateChangeStart);
314 |
315 | Object.keys(fakeXhr.eventListeners).forEach(function (event) {
316 | /*eslint-disable no-loop-func*/
317 | fakeXhr.eventListeners[event].forEach(function (handler) {
318 | xhr.addEventListener(event, handler.listener, {
319 | capture: handler.capture,
320 | once: handler.once,
321 | });
322 | });
323 | /*eslint-enable no-loop-func*/
324 | });
325 |
326 | xhr.addEventListener("readystatechange", stateChangeEnd);
327 | } else {
328 | xhr.onreadystatechange = stateChange;
329 | }
330 | apply(xhr, "open", xhrArgs);
331 | };
332 | FakeXMLHttpRequest.useFilters = false;
333 |
334 | function verifyRequestOpened(xhr) {
335 | if (xhr.readyState !== FakeXMLHttpRequest.OPENED) {
336 | const errorMessage =
337 | xhr.readyState === FakeXMLHttpRequest.UNSENT
338 | ? "INVALID_STATE_ERR - you might be trying to set the request state for a request that has already been aborted, it is recommended to check 'readyState' first..."
339 | : `INVALID_STATE_ERR - ${xhr.readyState}`;
340 | throw new Error(errorMessage);
341 | }
342 | }
343 |
344 | function verifyRequestSent(xhr) {
345 | if (xhr.readyState === FakeXMLHttpRequest.DONE) {
346 | throw new Error("Request done");
347 | }
348 | }
349 |
350 | function verifyHeadersReceived(xhr) {
351 | if (
352 | xhr.async &&
353 | xhr.readyState !== FakeXMLHttpRequest.HEADERS_RECEIVED
354 | ) {
355 | throw new Error("No headers received");
356 | }
357 | }
358 |
359 | function convertResponseBody(responseType, contentType, body) {
360 | if (responseType === "" || responseType === "text") {
361 | return body;
362 | } else if (supportsArrayBuffer && responseType === "arraybuffer") {
363 | return convertToArrayBuffer(body);
364 | } else if (responseType === "json") {
365 | try {
366 | return JSON.parse(body);
367 | } catch (e) {
368 | // Return parsing failure as null
369 | return null;
370 | }
371 | } else if (supportsBlob && responseType === "blob") {
372 | if (body instanceof Blob) {
373 | return body;
374 | }
375 |
376 | var blobOptions = {};
377 | if (contentType) {
378 | blobOptions.type = contentType;
379 | }
380 | return new Blob([convertToArrayBuffer(body)], blobOptions);
381 | } else if (responseType === "document") {
382 | if (isXmlContentType(contentType)) {
383 | return FakeXMLHttpRequest.parseXML(body);
384 | }
385 | return null;
386 | }
387 | throw new Error(`Invalid responseType ${responseType}`);
388 | }
389 |
390 | /**
391 | * Steps to follow when there is an error, according to:
392 | * https://xhr.spec.whatwg.org/#request-error-steps
393 | */
394 | function requestErrorSteps(xhr) {
395 | clearResponse(xhr);
396 | xhr.errorFlag = true;
397 | xhr.requestHeaders = {};
398 | xhr.responseHeaders = {};
399 |
400 | if (
401 | xhr.readyState !== FakeXMLHttpRequest.UNSENT &&
402 | xhr.sendFlag &&
403 | xhr.readyState !== FakeXMLHttpRequest.DONE
404 | ) {
405 | xhr.readyStateChange(FakeXMLHttpRequest.DONE);
406 | xhr.sendFlag = false;
407 | }
408 | }
409 |
410 | FakeXMLHttpRequest.parseXML = function parseXML(text) {
411 | // Treat empty string as parsing failure
412 | if (text !== "") {
413 | try {
414 | if (typeof DOMParser !== "undefined") {
415 | var parser = new DOMParser();
416 | var parsererrorNS = "";
417 |
418 | try {
419 | var parsererrors = parser
420 | .parseFromString("INVALID", "text/xml")
421 | .getElementsByTagName("parsererror");
422 | if (parsererrors.length) {
423 | parsererrorNS = parsererrors[0].namespaceURI;
424 | }
425 | } catch (e) {
426 | // passing invalid XML makes IE11 throw
427 | // so no namespace needs to be determined
428 | }
429 |
430 | var result;
431 | try {
432 | result = parser.parseFromString(text, "text/xml");
433 | } catch (err) {
434 | return null;
435 | }
436 |
437 | return result.getElementsByTagNameNS(
438 | parsererrorNS,
439 | "parsererror",
440 | ).length
441 | ? null
442 | : result;
443 | }
444 | var xmlDoc = new window.ActiveXObject("Microsoft.XMLDOM");
445 | xmlDoc.async = "false";
446 | xmlDoc.loadXML(text);
447 | return xmlDoc.parseError.errorCode !== 0 ? null : xmlDoc;
448 | } catch (e) {
449 | // Unable to parse XML - no biggie
450 | }
451 | }
452 |
453 | return null;
454 | };
455 |
456 | FakeXMLHttpRequest.statusCodes = {
457 | 100: "Continue",
458 | 101: "Switching Protocols",
459 | 200: "OK",
460 | 201: "Created",
461 | 202: "Accepted",
462 | 203: "Non-Authoritative Information",
463 | 204: "No Content",
464 | 205: "Reset Content",
465 | 206: "Partial Content",
466 | 207: "Multi-Status",
467 | 300: "Multiple Choice",
468 | 301: "Moved Permanently",
469 | 302: "Found",
470 | 303: "See Other",
471 | 304: "Not Modified",
472 | 305: "Use Proxy",
473 | 307: "Temporary Redirect",
474 | 400: "Bad Request",
475 | 401: "Unauthorized",
476 | 402: "Payment Required",
477 | 403: "Forbidden",
478 | 404: "Not Found",
479 | 405: "Method Not Allowed",
480 | 406: "Not Acceptable",
481 | 407: "Proxy Authentication Required",
482 | 408: "Request Timeout",
483 | 409: "Conflict",
484 | 410: "Gone",
485 | 411: "Length Required",
486 | 412: "Precondition Failed",
487 | 413: "Request Entity Too Large",
488 | 414: "Request-URI Too Long",
489 | 415: "Unsupported Media Type",
490 | 416: "Requested Range Not Satisfiable",
491 | 417: "Expectation Failed",
492 | 422: "Unprocessable Entity",
493 | 500: "Internal Server Error",
494 | 501: "Not Implemented",
495 | 502: "Bad Gateway",
496 | 503: "Service Unavailable",
497 | 504: "Gateway Timeout",
498 | 505: "HTTP Version Not Supported",
499 | };
500 |
501 | extend(FakeXMLHttpRequest.prototype, sinonEvent.EventTarget, {
502 | async: true,
503 |
504 | open: function open(method, url, async, username, password) {
505 | this.method = method;
506 | this.url = url;
507 | this.async = typeof async === "boolean" ? async : true;
508 | this.username = username;
509 | this.password = password;
510 | clearResponse(this);
511 | this.requestHeaders = {};
512 | this.sendFlag = false;
513 |
514 | if (FakeXMLHttpRequest.useFilters === true) {
515 | var xhrArgs = arguments;
516 | var defake = FakeXMLHttpRequest.filters.some(function (filter) {
517 | return filter.apply(this, xhrArgs);
518 | });
519 | if (defake) {
520 | FakeXMLHttpRequest.defake(this, arguments);
521 | return;
522 | }
523 | }
524 | this.readyStateChange(FakeXMLHttpRequest.OPENED);
525 | },
526 |
527 | readyStateChange: function readyStateChange(state) {
528 | this.readyState = state;
529 |
530 | var readyStateChangeEvent = new sinonEvent.Event(
531 | "readystatechange",
532 | false,
533 | false,
534 | this,
535 | );
536 | if (typeof this.onreadystatechange === "function") {
537 | try {
538 | this.onreadystatechange(readyStateChangeEvent);
539 | } catch (e) {
540 | this.logError("Fake XHR onreadystatechange handler", e);
541 | }
542 | }
543 |
544 | if (this.readyState !== FakeXMLHttpRequest.DONE) {
545 | this.dispatchEvent(readyStateChangeEvent);
546 | } else {
547 | var event, progress;
548 |
549 | if (this.timedOut || this.aborted || this.status === 0) {
550 | progress = { loaded: 0, total: 0 };
551 | event =
552 | (this.timedOut && "timeout") ||
553 | (this.aborted && "abort") ||
554 | "error";
555 | } else {
556 | progress = { loaded: 100, total: 100 };
557 | event = "load";
558 | }
559 |
560 | if (supportsProgress) {
561 | this.upload.dispatchEvent(
562 | new sinonEvent.ProgressEvent(
563 | "progress",
564 | progress,
565 | this,
566 | ),
567 | );
568 | this.upload.dispatchEvent(
569 | new sinonEvent.ProgressEvent(event, progress, this),
570 | );
571 | this.upload.dispatchEvent(
572 | new sinonEvent.ProgressEvent("loadend", progress, this),
573 | );
574 | }
575 |
576 | this.dispatchEvent(
577 | new sinonEvent.ProgressEvent("progress", progress, this),
578 | );
579 | this.dispatchEvent(
580 | new sinonEvent.ProgressEvent(event, progress, this),
581 | );
582 | this.dispatchEvent(readyStateChangeEvent);
583 | this.dispatchEvent(
584 | new sinonEvent.ProgressEvent("loadend", progress, this),
585 | );
586 | }
587 | },
588 |
589 | // Ref https://xhr.spec.whatwg.org/#the-setrequestheader()-method
590 | setRequestHeader: function setRequestHeader(header, value) {
591 | if (typeof value !== "string") {
592 | throw new TypeError(
593 | `By RFC7230, section 3.2.4, header values should be strings. Got ${typeof value}`,
594 | );
595 | }
596 | verifyState(this);
597 |
598 | var checkUnsafeHeaders = true;
599 | if (typeof this.unsafeHeadersEnabled === "function") {
600 | checkUnsafeHeaders = this.unsafeHeadersEnabled();
601 | }
602 |
603 | if (
604 | checkUnsafeHeaders &&
605 | (getHeader(unsafeHeaders, header) !== null ||
606 | /^(Sec-|Proxy-)/i.test(header))
607 | ) {
608 | throw new Error(
609 | // eslint-disable-next-line quotes
610 | `Refused to set unsafe header "${header}"`,
611 | );
612 | }
613 |
614 | // eslint-disable-next-line no-param-reassign
615 | value = normalizeHeaderValue(value);
616 |
617 | var existingHeader = getHeader(this.requestHeaders, header);
618 | if (existingHeader) {
619 | this.requestHeaders[existingHeader] += `, ${value}`;
620 | } else {
621 | this.requestHeaders[header] = value;
622 | }
623 | },
624 |
625 | setStatus: function setStatus(status) {
626 | var sanitizedStatus = typeof status === "number" ? status : 200;
627 |
628 | verifyRequestOpened(this);
629 | this.status = sanitizedStatus;
630 | this.statusText = FakeXMLHttpRequest.statusCodes[sanitizedStatus];
631 | },
632 |
633 | // Helps testing
634 | setResponseHeaders: function setResponseHeaders(headers) {
635 | verifyRequestOpened(this);
636 |
637 | var responseHeaders = (this.responseHeaders = {});
638 |
639 | Object.keys(headers).forEach(function (header) {
640 | responseHeaders[header] = headers[header];
641 | });
642 |
643 | if (this.async) {
644 | this.readyStateChange(FakeXMLHttpRequest.HEADERS_RECEIVED);
645 | } else {
646 | this.readyState = FakeXMLHttpRequest.HEADERS_RECEIVED;
647 | }
648 | },
649 |
650 | // Currently treats ALL data as a DOMString (i.e. no Document)
651 | send: function send(data) {
652 | verifyState(this);
653 |
654 | if (!/^(head)$/i.test(this.method)) {
655 | var contentType = getHeader(
656 | this.requestHeaders,
657 | "Content-Type",
658 | );
659 | if (this.requestHeaders[contentType]) {
660 | var value = this.requestHeaders[contentType].split(";");
661 | this.requestHeaders[contentType] =
662 | `${value[0]};charset=utf-8`;
663 | } else if (supportsFormData && !(data instanceof FormData)) {
664 | this.requestHeaders["Content-Type"] =
665 | "text/plain;charset=utf-8";
666 | }
667 |
668 | this.requestBody = data;
669 | }
670 |
671 | this.errorFlag = false;
672 | this.sendFlag = this.async;
673 | clearResponse(this);
674 |
675 | if (typeof this.onSend === "function") {
676 | this.onSend(this);
677 | }
678 |
679 | // Only listen if setInterval and Date are a stubbed.
680 | if (
681 | sinonXhr.supportsTimeout &&
682 | typeof setInterval.clock === "object" &&
683 | typeof Date.clock === "object"
684 | ) {
685 | var initiatedTime = Date.now();
686 | var self = this;
687 |
688 | // Listen to any possible tick by fake timers and check to see if timeout has
689 | // been exceeded. It's important to note that timeout can be changed while a request
690 | // is in flight, so we must check anytime the end user forces a clock tick to make
691 | // sure timeout hasn't changed.
692 | // https://xhr.spec.whatwg.org/#dfnReturnLink-2
693 | var clearIntervalId = setInterval(function () {
694 | // Check if the readyState has been reset or is done. If this is the case, there
695 | // should be no timeout. This will also prevent aborted requests and
696 | // fakeServerWithClock from triggering unnecessary responses.
697 | if (
698 | self.readyState === FakeXMLHttpRequest.UNSENT ||
699 | self.readyState === FakeXMLHttpRequest.DONE
700 | ) {
701 | clearInterval(clearIntervalId);
702 | } else if (
703 | typeof self.timeout === "number" &&
704 | self.timeout > 0
705 | ) {
706 | if (Date.now() >= initiatedTime + self.timeout) {
707 | self.triggerTimeout();
708 | clearInterval(clearIntervalId);
709 | }
710 | }
711 | }, 1);
712 | }
713 |
714 | this.dispatchEvent(
715 | new sinonEvent.Event("loadstart", false, false, this),
716 | );
717 | },
718 |
719 | abort: function abort() {
720 | this.aborted = true;
721 | requestErrorSteps(this);
722 | this.readyState = FakeXMLHttpRequest.UNSENT;
723 | },
724 |
725 | error: function () {
726 | clearResponse(this);
727 | this.errorFlag = true;
728 | this.requestHeaders = {};
729 | this.responseHeaders = {};
730 |
731 | this.readyStateChange(FakeXMLHttpRequest.DONE);
732 | },
733 |
734 | triggerTimeout: function triggerTimeout() {
735 | if (sinonXhr.supportsTimeout) {
736 | this.timedOut = true;
737 | requestErrorSteps(this);
738 | }
739 | },
740 |
741 | getResponseHeader: function getResponseHeader(header) {
742 | if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) {
743 | return null;
744 | }
745 |
746 | if (/^Set-Cookie2?$/i.test(header)) {
747 | return null;
748 | }
749 |
750 | // eslint-disable-next-line no-param-reassign
751 | header = getHeader(this.responseHeaders, header);
752 |
753 | return this.responseHeaders[header] || null;
754 | },
755 |
756 | getAllResponseHeaders: function getAllResponseHeaders() {
757 | if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) {
758 | return "";
759 | }
760 |
761 | var responseHeaders = this.responseHeaders;
762 | var headers = Object.keys(responseHeaders)
763 | .filter(excludeSetCookie2Header)
764 | .reduce(function (prev, header) {
765 | var value = responseHeaders[header];
766 |
767 | return `${prev}${header}: ${value}\r\n`;
768 | }, "");
769 |
770 | return headers;
771 | },
772 |
773 | setResponseBody: function setResponseBody(body) {
774 | verifyRequestSent(this);
775 | verifyHeadersReceived(this);
776 | verifyResponseBodyType(body, this.responseType);
777 | var contentType =
778 | this.overriddenMimeType ||
779 | this.getResponseHeader("Content-Type");
780 |
781 | var isTextResponse =
782 | this.responseType === "" || this.responseType === "text";
783 | clearResponse(this);
784 | if (this.async) {
785 | var chunkSize = this.chunkSize || 10;
786 | var index = 0;
787 |
788 | do {
789 | this.readyStateChange(FakeXMLHttpRequest.LOADING);
790 |
791 | if (isTextResponse) {
792 | this.responseText = this.response += body.substring(
793 | index,
794 | index + chunkSize,
795 | );
796 | }
797 | index += chunkSize;
798 | } while (index < body.length);
799 | }
800 |
801 | this.response = convertResponseBody(
802 | this.responseType,
803 | contentType,
804 | body,
805 | );
806 | if (isTextResponse) {
807 | this.responseText = this.response;
808 | }
809 |
810 | if (this.responseType === "document") {
811 | this.responseXML = this.response;
812 | } else if (
813 | this.responseType === "" &&
814 | isXmlContentType(contentType)
815 | ) {
816 | this.responseXML = FakeXMLHttpRequest.parseXML(
817 | this.responseText,
818 | );
819 | }
820 | this.readyStateChange(FakeXMLHttpRequest.DONE);
821 | },
822 |
823 | respond: function respond(status, headers, body) {
824 | this.responseURL = this.url;
825 |
826 | this.setStatus(status);
827 | this.setResponseHeaders(headers || {});
828 | this.setResponseBody(body || "");
829 | },
830 |
831 | uploadProgress: function uploadProgress(progressEventRaw) {
832 | if (supportsProgress) {
833 | this.upload.dispatchEvent(
834 | new sinonEvent.ProgressEvent(
835 | "progress",
836 | progressEventRaw,
837 | this.upload,
838 | ),
839 | );
840 | }
841 | },
842 |
843 | downloadProgress: function downloadProgress(progressEventRaw) {
844 | if (supportsProgress) {
845 | this.dispatchEvent(
846 | new sinonEvent.ProgressEvent(
847 | "progress",
848 | progressEventRaw,
849 | this,
850 | ),
851 | );
852 | }
853 | },
854 |
855 | uploadError: function uploadError(error) {
856 | if (supportsCustomEvent) {
857 | this.upload.dispatchEvent(
858 | new sinonEvent.CustomEvent("error", { detail: error }),
859 | );
860 | }
861 | },
862 |
863 | overrideMimeType: function overrideMimeType(type) {
864 | if (this.readyState >= FakeXMLHttpRequest.LOADING) {
865 | throw new Error("INVALID_STATE_ERR");
866 | }
867 | this.overriddenMimeType = type;
868 | },
869 | });
870 |
871 | var states = {
872 | UNSENT: 0,
873 | OPENED: 1,
874 | HEADERS_RECEIVED: 2,
875 | LOADING: 3,
876 | DONE: 4,
877 | };
878 |
879 | extend(FakeXMLHttpRequest, states);
880 | extend(FakeXMLHttpRequest.prototype, states);
881 |
882 | function useFakeXMLHttpRequest() {
883 | FakeXMLHttpRequest.restore = function restore(keepOnCreate) {
884 | if (sinonXhr.supportsXHR) {
885 | globalScope.XMLHttpRequest = sinonXhr.GlobalXMLHttpRequest;
886 | }
887 |
888 | if (sinonXhr.supportsActiveX) {
889 | globalScope.ActiveXObject = sinonXhr.GlobalActiveXObject;
890 | }
891 |
892 | delete FakeXMLHttpRequest.restore;
893 |
894 | if (keepOnCreate !== true) {
895 | delete FakeXMLHttpRequest.onCreate;
896 | }
897 | };
898 | if (sinonXhr.supportsXHR) {
899 | globalScope.XMLHttpRequest = FakeXMLHttpRequest;
900 | }
901 |
902 | if (sinonXhr.supportsActiveX) {
903 | globalScope.ActiveXObject = function ActiveXObject(objId) {
904 | if (
905 | objId === "Microsoft.XMLHTTP" ||
906 | /^Msxml2\.XMLHTTP/i.test(objId)
907 | ) {
908 | return new FakeXMLHttpRequest();
909 | }
910 |
911 | return new sinonXhr.GlobalActiveXObject(objId);
912 | };
913 | }
914 |
915 | return FakeXMLHttpRequest;
916 | }
917 |
918 | return {
919 | xhr: sinonXhr,
920 | FakeXMLHttpRequest: FakeXMLHttpRequest,
921 | useFakeXMLHttpRequest: useFakeXMLHttpRequest,
922 | };
923 | }
924 |
925 | module.exports = extend(fakeXMLHttpRequestFor(globalObject), {
926 | fakeXMLHttpRequestFor: fakeXMLHttpRequestFor,
927 | });
928 |
--------------------------------------------------------------------------------
/lib/fake-server/index.test.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var referee = require("@sinonjs/referee");
4 | var setupDOM = require("jsdom-global");
5 | var JSDOM = require("jsdom").JSDOM;
6 | var sinon = require("sinon");
7 | var sinonFakeServer = require("./index");
8 | var fakeXhr = require("../fake-xhr/");
9 | var FakeXMLHttpRequest = fakeXhr.FakeXMLHttpRequest;
10 |
11 | var JSDOMParser;
12 | if (JSDOM) {
13 | JSDOMParser = new JSDOM().window.DOMParser;
14 | }
15 |
16 | var assert = referee.assert;
17 | var refute = referee.refute;
18 |
19 | var supportsArrayBuffer = typeof ArrayBuffer !== "undefined";
20 |
21 | describe("sinonFakeServer", function () {
22 | beforeEach(function () {
23 | if (JSDOMParser) {
24 | global.DOMParser = JSDOMParser;
25 | this.cleanupDOM = setupDOM();
26 | }
27 | });
28 |
29 | afterEach(function () {
30 | if (this.server) {
31 | this.server.restore();
32 | }
33 |
34 | if (JSDOMParser) {
35 | delete global.DOMParser;
36 | this.cleanupDOM();
37 | }
38 | });
39 |
40 | it("provides restore method", function () {
41 | this.server = sinonFakeServer.create();
42 |
43 | assert.isFunction(this.server.restore);
44 | });
45 |
46 | describe(".create", function () {
47 | it("allows the 'autoRespond' setting", function () {
48 | var server = sinonFakeServer.create({
49 | autoRespond: true,
50 | });
51 | assert(
52 | server.autoRespond,
53 | "fakeServer.create should accept 'autoRespond' setting",
54 | );
55 | });
56 |
57 | it("allows the 'autoRespondAfter' setting", function () {
58 | var server = sinonFakeServer.create({
59 | autoRespondAfter: 500,
60 | });
61 | assert.equals(
62 | server.autoRespondAfter,
63 | 500,
64 | "fakeServer.create should accept 'autoRespondAfter' setting",
65 | );
66 | });
67 |
68 | it("allows the 'respondImmediately' setting", function () {
69 | var server = sinonFakeServer.create({
70 | respondImmediately: true,
71 | });
72 | assert(
73 | server.respondImmediately,
74 | "fakeServer.create should accept 'respondImmediately' setting",
75 | );
76 | });
77 |
78 | it("allows the 'fakeHTTPMethods' setting", function () {
79 | var server = sinonFakeServer.create({
80 | fakeHTTPMethods: true,
81 | });
82 | assert(
83 | server.fakeHTTPMethods,
84 | "fakeServer.create should accept 'fakeHTTPMethods' setting",
85 | );
86 | });
87 |
88 | it("allows the 'unsafeHeadersEnabled' setting", function () {
89 | var server = sinon.fakeServer.create({
90 | unsafeHeadersEnabled: false,
91 | });
92 | refute.isUndefined(
93 | server.unsafeHeadersEnabled,
94 | "'unsafeHeadersEnabled' expected to be defined at server level",
95 | );
96 | assert(
97 | !server.unsafeHeadersEnabled,
98 | "fakeServer.create should accept 'unsafeHeadersEnabled' setting",
99 | );
100 | });
101 |
102 | it("does not assign a non-allowlisted setting", function () {
103 | var server = sinonFakeServer.create({
104 | foo: true,
105 | });
106 | refute(
107 | server.foo,
108 | "fakeServer.create should not accept 'foo' settings",
109 | );
110 | });
111 | });
112 |
113 | it("fakes XMLHttpRequest", function () {
114 | var sandbox = sinon.createSandbox();
115 | sandbox.stub(fakeXhr, "useFakeXMLHttpRequest").returns({
116 | restore: sinon.stub(),
117 | });
118 |
119 | this.server = sinonFakeServer.create();
120 |
121 | assert(fakeXhr.useFakeXMLHttpRequest.called);
122 | sandbox.restore();
123 | });
124 |
125 | it("mirrors FakeXMLHttpRequest restore method", function () {
126 | var sandbox = sinon.createSandbox();
127 | this.server = sinonFakeServer.create();
128 | var restore = sandbox.stub(FakeXMLHttpRequest, "restore");
129 | this.server.restore();
130 |
131 | assert(restore.called);
132 | sandbox.restore();
133 | });
134 |
135 | describe(".requests", function () {
136 | beforeEach(function () {
137 | this.server = sinonFakeServer.create();
138 | });
139 |
140 | afterEach(function () {
141 | this.server.restore();
142 | });
143 |
144 | it("collects objects created with fake XHR", function () {
145 | var xhrs = [new FakeXMLHttpRequest(), new FakeXMLHttpRequest()];
146 |
147 | assert.equals(this.server.requests, xhrs);
148 | });
149 |
150 | it("collects xhr objects through addRequest", function () {
151 | this.server.addRequest = sinon.spy();
152 | var xhr = new FakeXMLHttpRequest();
153 |
154 | assert(this.server.addRequest.calledWith(xhr));
155 | });
156 |
157 | it("observes onSend on requests", function () {
158 | var xhrs = [new FakeXMLHttpRequest(), new FakeXMLHttpRequest()];
159 |
160 | assert.isFunction(xhrs[0].onSend);
161 | assert.isFunction(xhrs[1].onSend);
162 | });
163 |
164 | it("onSend should call handleRequest with request object", function () {
165 | var xhr = new FakeXMLHttpRequest();
166 | xhr.open("GET", "/");
167 | sinon.spy(this.server, "handleRequest");
168 |
169 | xhr.send();
170 |
171 | assert(this.server.handleRequest.called);
172 | assert(this.server.handleRequest.calledWith(xhr));
173 | });
174 | });
175 |
176 | describe(".handleRequest", function () {
177 | beforeEach(function () {
178 | this.server = sinonFakeServer.create();
179 | });
180 |
181 | afterEach(function () {
182 | this.server.restore();
183 | });
184 |
185 | it("responds to synchronous requests", function () {
186 | var xhr = new FakeXMLHttpRequest();
187 | xhr.open("GET", "/", false);
188 | sinon.spy(xhr, "respond");
189 |
190 | xhr.send();
191 |
192 | assert(xhr.respond.called);
193 | });
194 |
195 | it("does not respond to async requests", function () {
196 | var xhr = new FakeXMLHttpRequest();
197 | xhr.open("GET", "/", true);
198 | sinon.spy(xhr, "respond");
199 |
200 | xhr.send();
201 |
202 | assert.isFalse(xhr.respond.called);
203 | });
204 | });
205 |
206 | describe(".respondWith", function () {
207 | beforeEach(function () {
208 | this.sandbox = sinon.createSandbox();
209 |
210 | this.server = sinonFakeServer.create({
211 | setTimeout: this.sandbox.spy(),
212 | useImmediateExceptions: false,
213 | });
214 |
215 | this.getRootAsync = new FakeXMLHttpRequest();
216 | this.getRootAsync.open("GET", "/", true);
217 | this.getRootAsync.send();
218 | sinon.spy(this.getRootAsync, "respond");
219 |
220 | this.getRootAsyncArrayBuffer = new FakeXMLHttpRequest();
221 | this.getRootAsyncArrayBuffer.responseType = "arraybuffer";
222 | this.getRootAsyncArrayBuffer.open("GET", "/", true);
223 | this.getRootAsyncArrayBuffer.send();
224 | sinon.spy(this.getRootAsyncArrayBuffer, "respond");
225 |
226 | this.postRootAsync = new FakeXMLHttpRequest();
227 | this.postRootAsync.open("POST", "/", true);
228 | this.postRootAsync.send();
229 | sinon.spy(this.postRootAsync, "respond");
230 |
231 | this.getRootSync = new FakeXMLHttpRequest();
232 | this.getRootSync.open("GET", "/", false);
233 |
234 | this.getPathAsync = new FakeXMLHttpRequest();
235 | this.getPathAsync.open("GET", "/path", true);
236 | this.getPathAsync.send();
237 | sinon.spy(this.getPathAsync, "respond");
238 |
239 | this.postPathAsync = new FakeXMLHttpRequest();
240 | this.postPathAsync.open("POST", "/path", true);
241 | this.postPathAsync.send();
242 | sinon.spy(this.postPathAsync, "respond");
243 | });
244 |
245 | afterEach(function () {
246 | this.server.restore();
247 | this.sandbox.restore();
248 | });
249 |
250 | it("responds to queued async text requests", function () {
251 | this.server.respondWith("Oh yeah! Duffman!");
252 |
253 | this.server.respond();
254 |
255 | assert(this.getRootAsync.respond.called);
256 | assert.equals(this.getRootAsync.respond.args[0], [
257 | 200,
258 | {},
259 | "Oh yeah! Duffman!",
260 | ]);
261 | assert.equals(
262 | this.getRootAsync.readyState,
263 | FakeXMLHttpRequest.DONE,
264 | );
265 | });
266 |
267 | it("responds to all queued async requests", function () {
268 | this.server.respondWith("Oh yeah! Duffman!");
269 |
270 | this.server.respond();
271 |
272 | assert(this.getRootAsync.respond.called);
273 | assert(this.getPathAsync.respond.called);
274 | });
275 |
276 | it("does not respond to requests queued after respond() (eg from callbacks)", function () {
277 | var xhr;
278 | this.getRootAsync.addEventListener("load", function () {
279 | xhr = new FakeXMLHttpRequest();
280 | xhr.open("GET", "/", true);
281 | xhr.send();
282 | sinon.spy(xhr, "respond");
283 | });
284 |
285 | this.server.respondWith("Oh yeah! Duffman!");
286 |
287 | this.server.respond();
288 |
289 | assert(this.getRootAsync.respond.called);
290 | assert(this.getPathAsync.respond.called);
291 | assert(!xhr.respond.called);
292 |
293 | this.server.respond();
294 |
295 | assert(xhr.respond.called);
296 | });
297 |
298 | it("responds with status, headers, and text body", function () {
299 | var headers = { "Content-Type": "X-test" };
300 | this.server.respondWith([201, headers, "Oh yeah!"]);
301 |
302 | this.server.respond();
303 |
304 | assert(this.getRootAsync.respond.called);
305 | assert.equals(this.getRootAsync.respond.args[0], [
306 | 201,
307 | headers,
308 | "Oh yeah!",
309 | ]);
310 | assert.equals(
311 | this.getRootAsync.readyState,
312 | FakeXMLHttpRequest.DONE,
313 | );
314 | });
315 |
316 | it("handles responding with empty queue", function () {
317 | delete this.server.queue;
318 | var server = this.server;
319 |
320 | refute.exception(function () {
321 | server.respond();
322 | });
323 | });
324 |
325 | it("responds to sync request with canned answers", function () {
326 | this.server.respondWith([210, { "X-Ops": "Yeah" }, "Body, man"]);
327 |
328 | this.getRootSync.send();
329 |
330 | assert.equals(this.getRootSync.status, 210);
331 | assert.equals(
332 | this.getRootSync.getAllResponseHeaders(),
333 | "X-Ops: Yeah\r\n",
334 | );
335 | assert.equals(this.getRootSync.responseText, "Body, man");
336 | });
337 |
338 | it("responds to sync request with 404 if no response is set", function () {
339 | this.getRootSync.send();
340 |
341 | assert.equals(this.getRootSync.status, 404);
342 | assert.equals(this.getRootSync.getAllResponseHeaders(), "");
343 | assert.equals(this.getRootSync.responseText, "");
344 | });
345 |
346 | it("responds to async request with 404 if no response is set", function () {
347 | this.server.respond();
348 |
349 | assert.equals(this.getRootAsync.respond.args[0], [404, {}, ""]);
350 | });
351 |
352 | it("responds to specific URL", function () {
353 | this.server.respondWith("/path", "Duffman likes Duff beer");
354 |
355 | this.server.respond();
356 |
357 | assert.equals(this.getRootAsync.respond.args[0], [404, {}, ""]);
358 | assert.equals(this.getPathAsync.respond.args[0], [
359 | 200,
360 | {},
361 | "Duffman likes Duff beer",
362 | ]);
363 | });
364 |
365 | it("responds to URL matched by regexp", function () {
366 | this.server.respondWith(/^\/p.*/, "Regexp");
367 |
368 | this.server.respond();
369 |
370 | assert.equals(this.getPathAsync.respond.args[0], [
371 | 200,
372 | {},
373 | "Regexp",
374 | ]);
375 | });
376 |
377 | it("responds to URL matched by url matcher function", function () {
378 | this.server.respondWith(function () {
379 | return true;
380 | }, "FuncMatcher");
381 |
382 | this.server.respond();
383 |
384 | assert.equals(this.getPathAsync.respond.args[0], [
385 | 200,
386 | {},
387 | "FuncMatcher",
388 | ]);
389 | });
390 |
391 | it("does not respond to URL not matched by regexp", function () {
392 | this.server.respondWith(/^\/p.*/, "No regexp match");
393 |
394 | this.server.respond();
395 |
396 | assert.equals(this.getRootAsync.respond.args[0], [404, {}, ""]);
397 | });
398 |
399 | it("does not respond to URL not matched by function url matcher", function () {
400 | this.server.respondWith(function () {
401 | return false;
402 | }, "No function match");
403 |
404 | this.server.respond();
405 |
406 | assert.equals(this.getRootAsync.respond.args[0], [404, {}, ""]);
407 | });
408 |
409 | it("responds to all URLs matched by regexp", function () {
410 | this.server.respondWith(/^\/.*/, "Match all URLs");
411 |
412 | this.server.respond();
413 |
414 | assert.equals(this.getRootAsync.respond.args[0], [
415 | 200,
416 | {},
417 | "Match all URLs",
418 | ]);
419 | assert.equals(this.getPathAsync.respond.args[0], [
420 | 200,
421 | {},
422 | "Match all URLs",
423 | ]);
424 | });
425 |
426 | it("responds to all URLs matched by function matcher", function () {
427 | this.server.respondWith(function () {
428 | return true;
429 | }, "Match all URLs");
430 |
431 | this.server.respond();
432 |
433 | assert.equals(this.getRootAsync.respond.args[0], [
434 | 200,
435 | {},
436 | "Match all URLs",
437 | ]);
438 | assert.equals(this.getPathAsync.respond.args[0], [
439 | 200,
440 | {},
441 | "Match all URLs",
442 | ]);
443 | });
444 |
445 | it("responds to all requests when match URL is falsy", function () {
446 | this.server.respondWith("", "Falsy URL");
447 |
448 | this.server.respond();
449 |
450 | assert.equals(this.getRootAsync.respond.args[0], [
451 | 200,
452 | {},
453 | "Falsy URL",
454 | ]);
455 | assert.equals(this.getPathAsync.respond.args[0], [
456 | 200,
457 | {},
458 | "Falsy URL",
459 | ]);
460 | });
461 |
462 | it("responds to no requests when function matcher is falsy", function () {
463 | this.server.respondWith(function () {
464 | return false;
465 | }, "Falsy URL");
466 |
467 | this.server.respond();
468 |
469 | assert.equals(this.getRootAsync.respond.args[0], [404, {}, ""]);
470 | assert.equals(this.getPathAsync.respond.args[0], [404, {}, ""]);
471 | });
472 |
473 | it("responds to all GET requests", function () {
474 | this.server.respondWith("GET", "", "All GETs");
475 |
476 | this.server.respond();
477 |
478 | assert.equals(this.getRootAsync.respond.args[0], [
479 | 200,
480 | {},
481 | "All GETs",
482 | ]);
483 | assert.equals(this.getPathAsync.respond.args[0], [
484 | 200,
485 | {},
486 | "All GETs",
487 | ]);
488 | assert.equals(this.postRootAsync.respond.args[0], [404, {}, ""]);
489 | assert.equals(this.postPathAsync.respond.args[0], [404, {}, ""]);
490 | });
491 |
492 | it("responds to all 'get' requests (case-insensitivity)", function () {
493 | this.server.respondWith("get", "", "All GETs");
494 |
495 | this.server.respond();
496 |
497 | assert.equals(this.getRootAsync.respond.args[0], [
498 | 200,
499 | {},
500 | "All GETs",
501 | ]);
502 | assert.equals(this.getPathAsync.respond.args[0], [
503 | 200,
504 | {},
505 | "All GETs",
506 | ]);
507 | assert.equals(this.postRootAsync.respond.args[0], [404, {}, ""]);
508 | assert.equals(this.postPathAsync.respond.args[0], [404, {}, ""]);
509 | });
510 |
511 | it("responds to all PUT requests", function () {
512 | this.server.respondWith("PUT", "", "All PUTs");
513 |
514 | this.server.respond();
515 |
516 | assert.equals(this.getRootAsync.respond.args[0], [404, {}, ""]);
517 | assert.equals(this.getPathAsync.respond.args[0], [404, {}, ""]);
518 | assert.equals(this.postRootAsync.respond.args[0], [404, {}, ""]);
519 | assert.equals(this.postPathAsync.respond.args[0], [404, {}, ""]);
520 | });
521 |
522 | it("responds to all POST requests", function () {
523 | this.server.respondWith("POST", "", "All POSTs");
524 |
525 | this.server.respond();
526 |
527 | assert.equals(this.getRootAsync.respond.args[0], [404, {}, ""]);
528 | assert.equals(this.getPathAsync.respond.args[0], [404, {}, ""]);
529 | assert.equals(this.postRootAsync.respond.args[0], [
530 | 200,
531 | {},
532 | "All POSTs",
533 | ]);
534 | assert.equals(this.postPathAsync.respond.args[0], [
535 | 200,
536 | {},
537 | "All POSTs",
538 | ]);
539 | });
540 |
541 | it("responds to all POST requests to /path", function () {
542 | this.server.respondWith("POST", "/path", "All POSTs");
543 |
544 | this.server.respond();
545 |
546 | assert.equals(this.getRootAsync.respond.args[0], [404, {}, ""]);
547 | assert.equals(this.getPathAsync.respond.args[0], [404, {}, ""]);
548 | assert.equals(this.postRootAsync.respond.args[0], [404, {}, ""]);
549 | assert.equals(this.postPathAsync.respond.args[0], [
550 | 200,
551 | {},
552 | "All POSTs",
553 | ]);
554 | });
555 |
556 | it("responds to all POST requests matching regexp", function () {
557 | this.server.respondWith("POST", /^\/path(\?.*)?/, "All POSTs");
558 |
559 | this.server.respond();
560 |
561 | assert.equals(this.getRootAsync.respond.args[0], [404, {}, ""]);
562 | assert.equals(this.getPathAsync.respond.args[0], [404, {}, ""]);
563 | assert.equals(this.postRootAsync.respond.args[0], [404, {}, ""]);
564 | assert.equals(this.postPathAsync.respond.args[0], [
565 | 200,
566 | {},
567 | "All POSTs",
568 | ]);
569 | });
570 |
571 | it("does not respond to aborted requests", function () {
572 | this.server.respondWith("/", "That's my homepage!");
573 | this.getRootAsync.aborted = true;
574 |
575 | this.server.respond();
576 |
577 | assert.isFalse(this.getRootAsync.respond.called);
578 | });
579 |
580 | it("resets requests", function () {
581 | this.server.respondWith("/", "That's my homepage!");
582 |
583 | this.server.respond();
584 |
585 | assert.equals(this.server.queue, []);
586 | });
587 |
588 | it("notifies all requests when some throw", function () {
589 | this.getRootAsync.respond = function () {
590 | throw new Error("Oops!");
591 | };
592 |
593 | this.server.respondWith("");
594 | this.server.respond();
595 |
596 | assert.equals(this.getPathAsync.respond.args[0], [200, {}, ""]);
597 | assert.equals(this.postRootAsync.respond.args[0], [200, {}, ""]);
598 | assert.equals(this.postPathAsync.respond.args[0], [200, {}, ""]);
599 | });
600 |
601 | it("recognizes request with hostname", function () {
602 | // set the host value, as jsdom default is 'about:blank'
603 | setupDOM("", { url: "http://localhost/" });
604 | this.server.respondWith("/", [200, {}, "Yep"]);
605 | var xhr = new FakeXMLHttpRequest();
606 | var loc = window.location;
607 |
608 | xhr.open("GET", `${loc.protocol}//${loc.host}/`, true);
609 | xhr.send();
610 | sinon.spy(xhr, "respond");
611 |
612 | this.server.respond();
613 |
614 | assert.equals(xhr.respond.args[0], [200, {}, "Yep"]);
615 | });
616 |
617 | it("responds to matching paths with port number in external URL", function () {
618 | // setup server & client
619 | setupDOM("", { url: "http://localhost/" });
620 | var localAPI = "http://localhost:5000/ping";
621 | this.server.respondWith("GET", localAPI, "Pong");
622 |
623 | // Create fake client request
624 | var xhr = new FakeXMLHttpRequest();
625 | xhr.open("GET", localAPI, true);
626 | xhr.send();
627 | sinon.spy(xhr, "respond");
628 |
629 | this.server.respond();
630 |
631 | assert.equals(xhr.respond.args[0], [200, {}, "Pong"]);
632 | });
633 |
634 | it("responds to matching paths when port numbers are different", function () {
635 | // setup server & client
636 | setupDOM("", { url: "http://localhost:8080/" });
637 | var localAPI = "http://localhost:5000/ping";
638 | this.server.respondWith("GET", localAPI, "Pong");
639 | this.server.respondWith(
640 | "GET",
641 | "http://localhost:8080/ping",
642 | "Ding",
643 | );
644 |
645 | // Create fake client request
646 | var xhr = new FakeXMLHttpRequest();
647 | xhr.open("GET", localAPI, true);
648 | xhr.send();
649 | sinon.spy(xhr, "respond");
650 |
651 | this.server.respond();
652 |
653 | assert.equals(xhr.respond.args[0], [200, {}, "Pong"]);
654 | });
655 |
656 | it("responds although window.location is undefined", function () {
657 | // setup server & client
658 | this.cleanupDOM(); // remove window, Document, etc.
659 | var origin = "http://localhost";
660 | this.server.respondWith("GET", "/ping", "Pong");
661 |
662 | // Create fake client request
663 | var xhr = new FakeXMLHttpRequest();
664 | xhr.open("GET", `${origin}/ping`, true);
665 | xhr.send();
666 | sinon.spy(xhr, "respond");
667 |
668 | this.server.respond();
669 |
670 | assert.equals(xhr.respond.args[0], [200, {}, "Pong"]);
671 | });
672 |
673 | // React Native on Android places location on window.window
674 | it("responds as expected for React Native on Android with window.window.location", function () {
675 | this.cleanupDOM(); // remove default window, Document, etc.
676 | // setup client
677 | // Build Android like format (manual jsdom, with window.window inset)
678 | var html =
679 | // eslint-disable-next-line quotes
680 | '