├── .coveralls.yml ├── .gitignore ├── test ├── .jshintrc ├── disable_unhandled_test.js ├── registry_test.js ├── error_handler_test.js ├── undefined_response_test.js ├── shutdown_test.js ├── undefined_route_test.js ├── hosts_test.js ├── force_passthrough_test.js ├── creation_test.js ├── fetch_test.js ├── handler_test.js ├── url_parsing_test.js ├── requests_stored_test.js ├── passthrough_test.js └── calling_test.js ├── .prettierrc.js ├── .travis.yml ├── src ├── index.ts ├── registry.ts ├── hosts.ts ├── parse-url.ts ├── interceptor.ts ├── create-passthrough.ts └── pretender.ts ├── .release-it.json ├── bower.json ├── .eslintrc.js ├── .jshintrc ├── iife-wrapper.js ├── LICENSE ├── rollup.config.js ├── RELEASE.md ├── index.d.ts ├── package.json ├── CONDUCT.md ├── karma.conf.js ├── tsconfig.json ├── CHANGELOG.md └── README.md /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: $COVERALLS_REPO_TOKEN -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | coverage* 4 | dist 5 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "test", 4 | "modules" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | tabWidth: 2, 4 | singleQuote: true, 5 | }; 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "14" 4 | 5 | addons: 6 | chrome: stable 7 | 8 | script: 9 | - yarn run test-ci 10 | - NO_BUNDLE=true yarn run test-ci 11 | 12 | after_script: 13 | - find ./coverage/HeadlessChrome* -name "lcov.info" -exec cat {} \; | coveralls 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import parseURL from './parse-url'; 2 | import Registry from './registry'; 3 | import Hosts from './hosts'; 4 | import Pretender from './pretender'; 5 | 6 | Pretender.parseURL = parseURL; 7 | Pretender.Hosts = Hosts; 8 | Pretender.Registry = Registry; 9 | export default Pretender; 10 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "release-it-lerna-changelog": { 4 | "infile": "CHANGELOG.md", 5 | "launchEditor": true 6 | } 7 | }, 8 | "git": { 9 | "tagName": "v${version}" 10 | }, 11 | "npm": { 12 | "publish": true 13 | }, 14 | "github": { 15 | "release": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pretender", 3 | "version": "1.4.1", 4 | "authors": [ 5 | "Trek Glowacki " 6 | ], 7 | "license": "MIT", 8 | "ignore": [ 9 | "**/.*", 10 | "node_modules", 11 | "bower_components", 12 | "test", 13 | "tests" 14 | ], 15 | "main": [ 16 | "./pretender.js" 17 | ], 18 | "dependencies": { 19 | "FakeXMLHttpRequest": "^2.0.1", 20 | "route-recognizer": "^0.2.3" 21 | }, 22 | "devDependencies": { 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/disable_unhandled_test.js: -------------------------------------------------------------------------------- 1 | var describe = QUnit.module; 2 | var it = QUnit.test; 3 | 4 | describe('disable unhandled request', function(config) { 5 | config.beforeEach(function() { 6 | this.pretender = new Pretender({ disableUnhandled: true}); 7 | }); 8 | config.afterEach(function() { 9 | this.pretender.shutdown(); 10 | }); 11 | 12 | it('does not add unhandledRequest', function(assert) { 13 | $.ajax({ url: 'not-defined' }); 14 | 15 | var req = this.pretender.unhandledRequests; 16 | assert.equal(req.length, 0); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/registry_test.js: -------------------------------------------------------------------------------- 1 | var describe = QUnit.module; 2 | var it = QUnit.test; 3 | 4 | describe('Registry', function(config) { 5 | config.beforeEach(function() { 6 | this.registry = new Pretender.Registry(); 7 | }); 8 | 9 | it('has a "verbs" property', function(assert) { 10 | assert.ok(this.registry.verbs); 11 | }); 12 | 13 | it('supports all HTTP verbs', function(assert) { 14 | var verbs = ['GET', 'PUT', 'POST', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD']; 15 | for (var v = 0; v < verbs.length; v++) { 16 | assert.ok(this.registry.verbs[verbs[v]], 'supports ' + verbs[v]); 17 | } 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/error_handler_test.js: -------------------------------------------------------------------------------- 1 | var describe = QUnit.module; 2 | var it = QUnit.test; 3 | 4 | describe('pretender errored requests', function(config) { 5 | config.beforeEach(function() { 6 | this.pretender = new Pretender(); 7 | }); 8 | 9 | config.afterEach(function() { 10 | this.pretender.shutdown(); 11 | }); 12 | 13 | it('calls erroredRequest', function(assert) { 14 | this.pretender.get('/some/path', function() { 15 | throw new Error('something in this handler broke!'); 16 | }); 17 | 18 | this.pretender.erroredRequest = function(verb, path, request, error) { 19 | assert.ok(error); 20 | }; 21 | 22 | $.ajax({ url: '/some/path' }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/registry.ts: -------------------------------------------------------------------------------- 1 | import RouteRecognizer from 'route-recognizer'; 2 | 3 | /** 4 | * Registry 5 | * 6 | * A registry is a map of HTTP verbs to route recognizers. 7 | */ 8 | export default class Registry { 9 | 10 | public verbs; 11 | 12 | constructor(/* host */) { 13 | // Herein we keep track of RouteRecognizer instances 14 | // keyed by HTTP method. Feel free to add more as needed. 15 | this.verbs = { 16 | GET: new RouteRecognizer(), 17 | PUT: new RouteRecognizer(), 18 | POST: new RouteRecognizer(), 19 | DELETE: new RouteRecognizer(), 20 | PATCH: new RouteRecognizer(), 21 | HEAD: new RouteRecognizer(), 22 | OPTIONS: new RouteRecognizer() 23 | }; 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'parser': 'typescript-eslint-parser', 3 | 'globals': { 4 | 'Pretender': true, 5 | 'sinon': true, // karma-sinon 6 | }, 7 | 'env': { 8 | 'browser': true, 9 | 'es6': true, 10 | 'node': true, 11 | 'jquery': true, 12 | 'qunit': true, 13 | }, 14 | 'extends': 'eslint:recommended', 15 | 'parserOptions': { 16 | 'ecmaVersion': 2015, 17 | 'sourceType': 'module' 18 | }, 19 | 'rules': { 20 | 'no-unused-vars': 'off', 21 | 'indent': [ 22 | 'error', 23 | 2 24 | ], 25 | 'linebreak-style': [ 26 | 'error', 27 | 'unix' 28 | ], 29 | 'quotes': [ 30 | 'error', 31 | 'single' 32 | ], 33 | 'semi': [ 34 | 'error', 35 | 'always' 36 | ] 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /test/undefined_response_test.js: -------------------------------------------------------------------------------- 1 | var describe = QUnit.module; 2 | var it = QUnit.test; 3 | 4 | describe('retruning an undefined response', function(config) { 5 | config.beforeEach(function() { 6 | this.pretender = new Pretender(); 7 | }); 8 | config.afterEach(function() { 9 | this.pretender.shutdown(); 10 | }); 11 | 12 | it('calls erroredRequest callback', function(assert) { 13 | this.pretender.get('/some/path', function() { 14 | // return nothing 15 | }); 16 | 17 | this.pretender.erroredRequest = function(verb, path, request, error) { 18 | var message = 19 | 'Nothing returned by handler for ' + 20 | path + 21 | '. ' + 22 | 'Remember to `return [status, headers, body];` in your route handler.'; 23 | assert.equal(error.message, message); 24 | }; 25 | 26 | $.ajax({ url: '/some/path' }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "self", 4 | "require", 5 | "module", 6 | "define", 7 | "process" 8 | ], 9 | "globals": { 10 | "console":false 11 | }, 12 | "expr": true, 13 | "proto": true, 14 | "strict": true, 15 | "indent": 2, 16 | "camelcase": true, 17 | "node": false, 18 | "browser": true, 19 | "boss": true, 20 | "curly": true, 21 | "latedef": "nofunc", 22 | "debug": false, 23 | "devel": false, 24 | "eqeqeq": true, 25 | "evil": true, 26 | "forin": false, 27 | "immed": false, 28 | "laxbreak": false, 29 | "newcap": true, 30 | "noarg": true, 31 | "noempty": false, 32 | "quotmark": true, 33 | "nonew": false, 34 | "nomen": false, 35 | "onevar": false, 36 | "plusplus": false, 37 | "regexp": false, 38 | "undef": true, 39 | "unused": true, 40 | "sub": true, 41 | "trailing": true, 42 | "white": false, 43 | "eqnull": true, 44 | "esnext": false 45 | } 46 | -------------------------------------------------------------------------------- /test/shutdown_test.js: -------------------------------------------------------------------------------- 1 | var nativeXMLHttpRequest; 2 | var describe = QUnit.module; 3 | var it = QUnit.test; 4 | 5 | describe('pretender shutdown', function(config) { 6 | config.beforeEach(function() { 7 | nativeXMLHttpRequest = window.XMLHttpRequest; 8 | }); 9 | 10 | config.afterEach(function() { 11 | nativeXMLHttpRequest = null; 12 | }); 13 | 14 | it('restores the native XMLHttpRequest object', function(assert) { 15 | var pretender = new Pretender(); 16 | assert.notEqual(window.XMLHttpRequest, nativeXMLHttpRequest); 17 | 18 | pretender.shutdown(); 19 | assert.equal(window.XMLHttpRequest, nativeXMLHttpRequest); 20 | }); 21 | 22 | it('warns if requests attempt to respond after shutdown', function(assert) { 23 | var pretender = new Pretender(); 24 | var request = new XMLHttpRequest(); 25 | pretender.shutdown(); 26 | 27 | assert.throws(function() { 28 | request.send(); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/hosts.ts: -------------------------------------------------------------------------------- 1 | import Registry from './registry'; 2 | import parseURL from './parse-url'; 3 | 4 | /** 5 | * Hosts 6 | * 7 | * a map of hosts to Registries, ultimately allowing 8 | * a per-host-and-port, per HTTP verb lookup of RouteRecognizers 9 | */ 10 | export default class Hosts { 11 | 12 | private registries = {}; 13 | 14 | /** 15 | * Hosts#forURL - retrieve a map of HTTP verbs to RouteRecognizers 16 | * for a given URL 17 | * 18 | * @param {String} url a URL 19 | * @return {Registry} a map of HTTP verbs to RouteRecognizers 20 | * corresponding to the provided URL's 21 | * hostname and port 22 | */ 23 | forURL(url: string): Registry { 24 | let host = parseURL(url).host; 25 | let registry = this.registries[host]; 26 | 27 | if (registry === undefined) { 28 | registry = (this.registries[host] = new Registry(/*host*/)); 29 | } 30 | 31 | return registry.verbs; 32 | } 33 | } -------------------------------------------------------------------------------- /iife-wrapper.js: -------------------------------------------------------------------------------- 1 | var Pretender = (function(self) { 2 | function getModuleDefault(module) { 3 | return module.default || module; 4 | } 5 | 6 | var appearsBrowserified = 7 | typeof self !== 'undefined' && 8 | typeof process !== 'undefined' && 9 | (Object.prototype.toString.call(process) === '[object Object]' || 10 | Object.prototype.toString.call(process) === '[object process]'); 11 | 12 | var RouteRecognizer = appearsBrowserified 13 | ? getModuleDefault(require('route-recognizer')) 14 | : self.RouteRecognizer; 15 | var FakeXMLHttpRequest = appearsBrowserified 16 | ? getModuleDefault(require('fake-xml-http-request')) 17 | : self.FakeXMLHttpRequest; 18 | 19 | /*==ROLLUP_CONTENT==*/ 20 | 21 | if (typeof module === 'object') { 22 | module.exports = Pretender; 23 | } else if (typeof define !== 'undefined') { 24 | define('pretender', [], function() { 25 | return Pretender; 26 | }); 27 | } 28 | 29 | self.Pretender = Pretender; 30 | 31 | return Pretender; 32 | })(self); 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Trek Glowacki 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /test/undefined_route_test.js: -------------------------------------------------------------------------------- 1 | var describe = QUnit.module; 2 | var it = QUnit.test; 3 | 4 | describe('route not defined', function(config) { 5 | config.beforeEach(function() { 6 | this.pretender = new Pretender(); 7 | }); 8 | config.afterEach(function() { 9 | this.pretender.shutdown(); 10 | }); 11 | 12 | it('calls unhandledRequest', function(assert) { 13 | this.pretender.unhandledRequest = function(verb, path) { 14 | assert.equal('GET', verb); 15 | assert.equal('not-defined', path); 16 | assert.ok(true); 17 | }; 18 | 19 | $.ajax({ 20 | url: 'not-defined', 21 | }); 22 | }); 23 | 24 | it('errors by default', function(assert) { 25 | var pretender = this.pretender; 26 | var verb = 'GET'; 27 | var path = '/foo/bar'; 28 | assert.throws(function() { 29 | pretender.unhandledRequest(verb, path); 30 | }, 'Pretender intercepted GET /foo/bar but no handler was defined for this type of request'); 31 | }); 32 | 33 | it('adds the request to the array of unhandled requests by default', function( 34 | assert 35 | ) { 36 | $.ajax({ 37 | url: 'not-defined', 38 | }); 39 | 40 | var req = this.pretender.unhandledRequests[0]; 41 | assert.equal(req.url, 'not-defined'); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from "@rollup/plugin-commonjs"; 2 | import resolve from "@rollup/plugin-node-resolve"; 3 | import typescript from "rollup-plugin-typescript"; 4 | import { readFileSync } from "fs"; 5 | import pkg from "./package.json"; 6 | 7 | const globals = { 8 | "fake-xml-http-request": "FakeXMLHttpRequest", 9 | "route-recognizer": "RouteRecognizer", 10 | "url-parse": "urlParse", 11 | }; 12 | 13 | const rollupTemplate = readFileSync("./iife-wrapper.js").toString(); 14 | const [banner, footer] = rollupTemplate.split("/*==ROLLUP_CONTENT==*/"); 15 | 16 | module.exports = [ 17 | { 18 | input: "src/index.ts", 19 | external: Object.keys(pkg.dependencies), 20 | output: [ 21 | { 22 | name: "Pretender", 23 | file: pkg.main, 24 | format: "iife", 25 | globals, 26 | banner, 27 | footer, 28 | }, 29 | { 30 | file: pkg.module, 31 | format: "es", 32 | }, 33 | ], 34 | plugins: [commonjs(), resolve(), typescript()], 35 | }, 36 | { 37 | input: "src/index.ts", 38 | output: [ 39 | { 40 | file: "./dist/pretender.bundle.js", 41 | name: "Pretender", 42 | format: "iife", 43 | } 44 | ], 45 | plugins: [commonjs(), resolve(), typescript()], 46 | }, 47 | ]; 48 | -------------------------------------------------------------------------------- /test/hosts_test.js: -------------------------------------------------------------------------------- 1 | var hosts; 2 | var describe = QUnit.module; 3 | var it = QUnit.test; 4 | 5 | describe('Hosts', function(config) { 6 | config.beforeEach(function() { 7 | hosts = new Pretender.Hosts(); 8 | }); 9 | 10 | config.afterEach(function() { 11 | hosts = undefined; 12 | }); 13 | 14 | describe('#forURL', function() { 15 | it('returns a registry for a URL', function(assert) { 16 | assert.ok(hosts.forURL('http://www.groupon.com/offers/skydiving')); 17 | }); 18 | 19 | it('returns a registry for a relative path', function(assert) { 20 | assert.ok(hosts.forURL('/offers/skydiving')); 21 | }); 22 | 23 | it('returns different Registry objects for different hosts ', function( 24 | assert 25 | ) { 26 | var registry1 = hosts.forURL('/offers/dinner_out'); 27 | var registry2 = hosts.forURL('http://www.yahoo.com/offers/dinner_out'); 28 | registry1.GET.add({ 29 | path: 'http://www.yahoo.com/offers/dinner_out', 30 | handler: function() { 31 | return [200, {}, 'ok']; 32 | }, 33 | }); 34 | assert.ok( 35 | !registry2.GET.recognize('http://www.yahoo.com/offers/dinner_out') 36 | ); 37 | assert.ok( 38 | !registry1.GET.recognize('http://www.yahoo.com/offers/dinner_out') 39 | ); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/force_passthrough_test.js: -------------------------------------------------------------------------------- 1 | var describe = QUnit.module; 2 | var it = QUnit.test; 3 | 4 | describe('force passthrough requests', function(config) { 5 | config.beforeEach(function() { 6 | this.pretender = new Pretender({ forcePassthrough: true }); 7 | }); 8 | 9 | config.afterEach(function() { 10 | this.pretender.shutdown(); 11 | }); 12 | 13 | it('passthrough request when forcePassthrough is true', function(assert) { 14 | var done = assert.async(); 15 | 16 | var passthroughInvoked = false; 17 | this.pretender.passthroughRequest = function(verb, path/*, request*/) { 18 | passthroughInvoked = true; 19 | assert.equal(verb, 'GET'); 20 | assert.equal(path, '/some/path'); 21 | }; 22 | 23 | $.ajax({ 24 | url: '/some/path', 25 | error: function(xhr) { 26 | assert.equal(xhr.status, 404); 27 | assert.ok(passthroughInvoked); 28 | done(); 29 | } 30 | }); 31 | }); 32 | 33 | it('unhandle request when forcePassthrough is false', function(assert) { 34 | var pretender = this.pretender; 35 | pretender.forcePassthrough = false; 36 | 37 | this.pretender.unhandledRequest = function(verb, path/*, request*/) { 38 | assert.equal(verb, 'GET'); 39 | assert.equal(path, '/some/path'); 40 | }; 41 | 42 | $.ajax({ url: '/some/path' }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/parse-url.ts: -------------------------------------------------------------------------------- 1 | import urlParse from 'url-parse'; 2 | /** 3 | * parseURL - decompose a URL into its parts 4 | * @param {String} url a URL 5 | * @return {Object} parts of the URL, including the following 6 | * 7 | * 'https://www.yahoo.com:1234/mypage?test=yes#abc' 8 | * 9 | * { 10 | * host: 'www.yahoo.com:1234', 11 | * protocol: 'https:', 12 | * search: '?test=yes', 13 | * hash: '#abc', 14 | * href: 'https://www.yahoo.com:1234/mypage?test=yes#abc', 15 | * pathname: '/mypage', 16 | * fullpath: '/mypage?test=yes' 17 | * } 18 | */ 19 | export default function parseURL(url: string) { 20 | let parsedUrl = new urlParse(url); 21 | 22 | if (!parsedUrl.host) { 23 | // eslint-disable-next-line no-self-assign 24 | parsedUrl.href = parsedUrl.href; // IE: load the host and protocol 25 | } 26 | 27 | var pathname = parsedUrl.pathname; 28 | if (pathname.charAt(0) !== '/') { 29 | pathname = '/' + pathname; // IE: prepend leading slash 30 | } 31 | 32 | var host = parsedUrl.host; 33 | if (parsedUrl.port === '80' || parsedUrl.port === '443') { 34 | host = parsedUrl.hostname; // IE: remove default port 35 | } 36 | 37 | return { 38 | host: host, 39 | protocol: parsedUrl.protocol, 40 | search: parsedUrl.query, 41 | hash: parsedUrl.hash, 42 | href: parsedUrl.href, 43 | pathname: pathname, 44 | fullpath: pathname + (parsedUrl.query || '') + (parsedUrl.hash || '') 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /test/creation_test.js: -------------------------------------------------------------------------------- 1 | var pretender; 2 | var describe = QUnit.module; 3 | var it = QUnit.test; 4 | 5 | describe('pretender creation', function(config) { 6 | config.afterEach(function() { 7 | if (pretender) { 8 | pretender.shutdown(); 9 | } 10 | pretender = null; 11 | }); 12 | 13 | it('a mapping function is optional', function(assert) { 14 | try { 15 | pretender = new Pretender(); 16 | } catch (e) { 17 | assert.ok(false); 18 | } 19 | 20 | assert.ok(true, 'does not raise'); 21 | }); 22 | 23 | it('many maps can be passed on creation', function(assert) { 24 | var aWasCalled = false; 25 | var bWasCalled = false; 26 | 27 | var mapA = function() { 28 | this.get('/some/path', function() { 29 | aWasCalled = true; 30 | }); 31 | }; 32 | 33 | var mapB = function() { 34 | this.get('/other/path', function() { 35 | bWasCalled = true; 36 | }); 37 | }; 38 | 39 | pretender = new Pretender(mapA, mapB); 40 | 41 | $.ajax({ url: '/some/path' }); 42 | $.ajax({ url: '/other/path' }); 43 | 44 | assert.ok(aWasCalled); 45 | assert.ok(bWasCalled); 46 | }); 47 | 48 | it('an error is thrown when a request handler is missing', function(assert) { 49 | assert.throws(function() { 50 | pretender = new Pretender(); 51 | pretender.get('/path', undefined); 52 | }, 'The function you tried passing to Pretender to handle GET /path is undefined or missing.'); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release 2 | 3 | Releases are mostly automated using 4 | [release-it](https://github.com/release-it/release-it/) and 5 | [lerna-changelog](https://github.com/lerna/lerna-changelog/). 6 | 7 | 8 | ## Preparation 9 | 10 | Since the majority of the actual release process is automated, the primary 11 | remaining task prior to releasing is confirming that all pull requests that 12 | have been merged since the last release have been labeled with the appropriate 13 | `lerna-changelog` labels and the titles have been updated to ensure they 14 | represent something that would make sense to our users. Some great information 15 | on why this is important can be found at 16 | [keepachangelog.com](https://keepachangelog.com/en/1.0.0/), but the overall 17 | guiding principles here is that changelogs are for humans, not machines. 18 | 19 | When reviewing merged PR's the labels to be used are: 20 | 21 | * breaking - Used when the PR is considered a breaking change. 22 | * enhancement - Used when the PR adds a new feature or enhancement. 23 | * bug - Used when the PR fixes a bug included in a previous release. 24 | * documentation - Used when the PR adds or updates documentation. 25 | * internal - Used for internal changes that still require a mention in the 26 | changelog/release notes. 27 | 28 | 29 | ## Release 30 | 31 | Once the prep work is completed, the actual release is straight forward: 32 | 33 | ``` 34 | yarn install 35 | yarn release 36 | ``` 37 | 38 | The `release` script leverages 39 | [release-it](https://github.com/release-it/release-it/) to do the mechanical 40 | release process. It will prompt you through the process of choosing the version 41 | number, tagging, pushing the tag and commits, etc. 42 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import FakeXMLHttpRequest from 'fake-xml-http-request'; 2 | import { Params, QueryParams } from 'route-recognizer'; 3 | 4 | type SetupCallback = (this: Server) => void; 5 | interface SetupConfig { 6 | forcePassthrough: boolean; 7 | } 8 | export type Config = SetupCallback | SetupConfig; 9 | export class Server { 10 | public passthrough: ResponseHandler; 11 | 12 | constructor(config?: Config); 13 | // HTTP request verbs 14 | public get: RequestHandler; 15 | public put: RequestHandler; 16 | public post: RequestHandler; 17 | public patch: RequestHandler; 18 | public delete: RequestHandler; 19 | public options: RequestHandler; 20 | public head: RequestHandler; 21 | 22 | public shutdown(): void; 23 | 24 | public map(maps: Function): void; 25 | 26 | public handledRequest(verb: string, path: string, request: FakeXMLHttpRequest & ExtraRequestData): void; 27 | public unhandledRequest(verb: string, path: string, request: FakeXMLHttpRequest & ExtraRequestData): void; 28 | public passthroughRequest(verb: string, path: string, request: FakeXMLHttpRequest & ExtraRequestData): void; 29 | public erroredRequest(verb: string, path: string, request: FakeXMLHttpRequest & ExtraRequestData, error: Error): void; 30 | 31 | public prepareBody(body: string): string; 32 | public prepareHeaders(headers: {[k: string]: string}): {[k: string]: string}; 33 | } 34 | 35 | export type RequestHandler = ( 36 | urlExpression: string, 37 | response: ResponseHandler, 38 | asyncOrDelay?: boolean | number 39 | ) => ResponseHandlerInstance; 40 | 41 | export type ResponseData = [number, { [k: string]: string }, string]; 42 | interface ExtraRequestData { 43 | params: Params; 44 | queryParams: QueryParams; 45 | } 46 | export type ResponseHandler = { 47 | (request: FakeXMLHttpRequest & ExtraRequestData): 48 | | ResponseData 49 | | PromiseLike; 50 | }; 51 | 52 | export type ResponseHandlerInstance = ResponseHandler & { 53 | async: boolean; 54 | numberOfCalls: number; 55 | } 56 | 57 | export default Server; 58 | -------------------------------------------------------------------------------- /test/fetch_test.js: -------------------------------------------------------------------------------- 1 | var describe = QUnit.module; 2 | var it = QUnit.test; 3 | 4 | describe('pretender invoking by fetch', function(config) { 5 | config.beforeEach(function() { 6 | this.pretender = new Pretender(); 7 | }); 8 | 9 | config.afterEach(function() { 10 | this.pretender.shutdown(); 11 | }); 12 | 13 | it('fetch triggers pretender', function(assert) { 14 | assert.expect(1); 15 | var wasCalled; 16 | 17 | this.pretender.get('/some/path', function() { 18 | wasCalled = true; 19 | return [200, {}, '']; 20 | }); 21 | 22 | var wait = fetch('/some/path'); 23 | assert.ok(wasCalled); 24 | return wait; 25 | }); 26 | 27 | it('is resolved asynchronously', function(assert) { 28 | assert.expect(2); 29 | var val = 'unset'; 30 | 31 | this.pretender.get('/some/path', function() { 32 | return [200, {}, '']; 33 | }); 34 | 35 | var wait = fetch('/some/path').then(function() { 36 | assert.equal(val, 'set'); 37 | }); 38 | 39 | assert.equal(val, 'unset'); 40 | val = 'set'; 41 | 42 | return wait; 43 | }); 44 | 45 | it('can NOT be resolved synchronously', function(assert) { 46 | assert.expect(2); 47 | var val = 'unset'; 48 | 49 | this.pretender.get( 50 | '/some/path', 51 | function() { 52 | return [200, {}, '']; 53 | }, 54 | false 55 | ); 56 | 57 | // This is async even we specified pretender get to be synchronised 58 | var wait = fetch('/some/path').then(function() { 59 | assert.equal(val, 'set'); 60 | }); 61 | assert.equal(val, 'unset'); 62 | val = 'set'; 63 | return wait; 64 | }); 65 | 66 | it('has Abortable fetch', function(assert) { 67 | assert.expect(1); 68 | this.pretender.get( 69 | '/downloads', 70 | function(/*request*/) { 71 | return [200, {}, 'FAIL']; 72 | }, 73 | 200 74 | ); 75 | 76 | var controller = new AbortController(); 77 | var signal = controller.signal; 78 | setTimeout(function() { 79 | controller.abort(); 80 | }, 10); 81 | 82 | return fetch('/downloads', { signal: signal }) 83 | .catch(function(err) { 84 | assert.equal(err.name, 'AbortError'); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pretender", 3 | "version": "3.4.7", 4 | "main": "./dist/pretender.js", 5 | "module": "./dist/pretender.es.js", 6 | "types": "index.d.ts", 7 | "description": "Pretender is a mock server library for XMLHttpRequest and Fetch, that comes with an express/sinatra style syntax for defining routes and their handlers.", 8 | "license": "MIT", 9 | "scripts": { 10 | "release": "release-it", 11 | "prepublishOnly": "npm run build && npm run tests-only", 12 | "build": "rollup --config", 13 | "test": "npm run build && npm run eslint && npm run tests-only", 14 | "test-ci": "npm run build && npm run eslint && npm run tests-only-ci", 15 | "tests-only": "karma start --single-run", 16 | "tests-only-ci": "karma start --single-run --browsers ChromeHeadlessNoSandbox", 17 | "eslint": "eslint src/**/*.ts test", 18 | "test:server": "karma start --no-single-run" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/pretenderjs/pretender.git" 23 | }, 24 | "publishConfig": { 25 | "registry": "https://registry.npmjs.org/" 26 | }, 27 | "devDependencies": { 28 | "@rollup/plugin-commonjs": "^11.1.0", 29 | "@rollup/plugin-node-resolve": "^7.1.3", 30 | "abortcontroller-polyfill": "^1.1.9", 31 | "coveralls": "^3.1.0", 32 | "es6-promise": "^4.0.5", 33 | "eslint": "^5.12.0", 34 | "karma": "^5.0.6", 35 | "karma-chrome-launcher": "^3.1.0", 36 | "karma-coverage": "^2.0.2", 37 | "karma-jquery": "^0.2.4", 38 | "karma-qunit": "^4.1.1", 39 | "karma-sinon": "^1.0.5", 40 | "qunit": "^2.10.0", 41 | "release-it": "^13.6.0", 42 | "release-it-lerna-changelog": "^2.3.0", 43 | "rollup": "^2.10.2", 44 | "rollup-plugin-typescript": "^1.0.0", 45 | "sinon": "^9.0.2", 46 | "tslib": "^1.9.3", 47 | "typescript": "~3.1.1", 48 | "typescript-eslint-parser": "^21.0.2", 49 | "url-parse": "^1.5.3", 50 | "whatwg-fetch": "^3.6.2" 51 | }, 52 | "dependencies": { 53 | "fake-xml-http-request": "^2.1.2", 54 | "route-recognizer": "^0.3.3" 55 | }, 56 | "files": [ 57 | "dist", 58 | "index.d.ts" 59 | ], 60 | "jspm": { 61 | "shim": { 62 | "pretender": { 63 | "deps": [ 64 | "route-recognizer", 65 | "fake-xml-http-request" 66 | ], 67 | "exports": "Pretender" 68 | } 69 | } 70 | }, 71 | "volta": { 72 | "node": "16.5.0", 73 | "yarn": "1.22.4" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This Code of Conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at trek.glowacki@gmail.com. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | 45 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 46 | version 1.3.0, available at 47 | [http://contributor-covenant.org/version/1/3/0/][version] 48 | 49 | [homepage]: http://contributor-covenant.org 50 | [version]: http://contributor-covenant.org/version/1/3/0/ -------------------------------------------------------------------------------- /test/handler_test.js: -------------------------------------------------------------------------------- 1 | var describe = QUnit.module; 2 | var it = QUnit.test; 3 | 4 | describe('pretender adding a handler', function(config) { 5 | config.beforeEach(function() { 6 | this.pretender = new Pretender(); 7 | }); 8 | 9 | config.afterEach(function() { 10 | this.pretender.shutdown(); 11 | }); 12 | 13 | it('a handler is returned', function(assert) { 14 | var handler = this.pretender.get('/some/path', function() {}); 15 | assert.ok(handler); 16 | }); 17 | 18 | it('.get registers a handler for GET', function(assert) { 19 | var wasCalled; 20 | 21 | this.pretender.get('/some/path', function() { 22 | wasCalled = true; 23 | }); 24 | 25 | $.ajax({ url: '/some/path' }); 26 | assert.ok(wasCalled); 27 | }); 28 | 29 | it('.post registers a handler for POST', function(assert) { 30 | var wasCalled; 31 | 32 | this.pretender.post('/some/path', function() { 33 | wasCalled = true; 34 | }); 35 | 36 | $.ajax({ url: '/some/path', method: 'post' }); 37 | assert.ok(wasCalled); 38 | }); 39 | 40 | it('.patch registers a handler for PATCH', function(assert) { 41 | var wasCalled; 42 | 43 | this.pretender.patch('/some/path', function() { 44 | wasCalled = true; 45 | }); 46 | 47 | $.ajax({ url: '/some/path', method: 'patch' }); 48 | assert.ok(wasCalled); 49 | }); 50 | 51 | it('.delete registers a handler for DELETE', function(assert) { 52 | var wasCalled; 53 | 54 | this.pretender.delete('/some/path', function() { 55 | wasCalled = true; 56 | }); 57 | 58 | $.ajax({ url: '/some/path', method: 'delete' }); 59 | assert.ok(wasCalled); 60 | }); 61 | 62 | it('.options registers a handler for OPTIONS', function(assert) { 63 | var wasCalled; 64 | 65 | this.pretender.options('/some/path', function() { 66 | wasCalled = true; 67 | }); 68 | 69 | $.ajax({ url: '/some/path', method: 'options' }); 70 | assert.ok(wasCalled); 71 | }); 72 | 73 | it('.put registers a handler for PUT', function(assert) { 74 | var wasCalled; 75 | 76 | this.pretender.put('/some/path', function() { 77 | wasCalled = true; 78 | }); 79 | 80 | $.ajax({ url: '/some/path', method: 'put' }); 81 | assert.ok(wasCalled); 82 | }); 83 | 84 | it('.head registers a handler for HEAD', function(assert) { 85 | var wasCalled; 86 | 87 | this.pretender.head('/some/path', function() { 88 | wasCalled = true; 89 | }); 90 | 91 | $.ajax({ url: '/some/path', method: 'head' }); 92 | assert.ok(wasCalled); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /test/url_parsing_test.js: -------------------------------------------------------------------------------- 1 | var describe = QUnit.module; 2 | var it = QUnit.test; 3 | 4 | describe('parseURL', function() { 5 | var parseURL = Pretender.parseURL; 6 | 7 | function testUrl(url, expectedParts) { 8 | it('pathname, fullpath, protocol, hash and search are correct', function( 9 | assert 10 | ) { 11 | var parts = parseURL(url); 12 | assert.ok(parts, 'Parts exist'); 13 | 14 | assert.equal( 15 | parts.protocol, 16 | expectedParts.protocol, 17 | 'protocol should be "' + expectedParts.protocol + '"' 18 | ); 19 | assert.equal( 20 | parts.pathname, 21 | expectedParts.pathname, 22 | 'pathname should be "' + expectedParts.pathname + '"' 23 | ); 24 | assert.equal( 25 | parts.host, 26 | expectedParts.host, 27 | 'hostname is equal to "' + expectedParts.host + '"' 28 | ); 29 | assert.equal( 30 | parts.search, 31 | expectedParts.search, 32 | 'search should be "' + expectedParts.search + '"' 33 | ); 34 | assert.equal( 35 | parts.hash, 36 | expectedParts.hash, 37 | 'hash should be "' + expectedParts.search + '"' 38 | ); 39 | assert.equal( 40 | parts.fullpath, 41 | expectedParts.fullpath, 42 | 'fullpath should be "' + expectedParts.fullpath + '"' 43 | ); 44 | }); 45 | } 46 | 47 | describe('relative HTTP URLs', function() { 48 | testUrl('/mock/my/request?test=abc#def', { 49 | protocol: 'http:', 50 | pathname: '/mock/my/request', 51 | host: window.location.host, 52 | search: '?test=abc', 53 | hash: '#def', 54 | fullpath: '/mock/my/request?test=abc#def', 55 | }); 56 | }); 57 | 58 | describe('same-origin absolute HTTP URLs', function() { 59 | testUrl( 60 | window.location.protocol + 61 | '//' + 62 | window.location.host + 63 | '/mock/my/request?test=abc#def', 64 | { 65 | protocol: window.location.protocol, 66 | pathname: '/mock/my/request', 67 | host: window.location.host, 68 | search: '?test=abc', 69 | hash: '#def', 70 | fullpath: '/mock/my/request?test=abc#def', 71 | } 72 | ); 73 | }); 74 | 75 | describe('cross-origin absolute HTTP URLs', function() { 76 | testUrl('https://www.yahoo.com/mock/my/request?test=abc', { 77 | protocol: 'https:', 78 | pathname: '/mock/my/request', 79 | host: 'www.yahoo.com', 80 | search: '?test=abc', 81 | hash: '', 82 | fullpath: '/mock/my/request?test=abc', 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Thu Oct 06 2016 14:24:14 GMT+0800 (PHT) 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | // base path that will be used to resolve all patterns (eg. files, exclude) 7 | basePath: "", 8 | 9 | plugins: [ 10 | "karma-jquery", 11 | "karma-qunit", 12 | "karma-coverage", 13 | "karma-sinon", 14 | "karma-chrome-launcher", 15 | ], 16 | 17 | // frameworks to use 18 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 19 | frameworks: ["qunit", "sinon", "jquery-3.4.0"], 20 | 21 | // list of files / patterns to load in the browser 22 | files: (process.env.NO_BUNDLE 23 | ? [ 24 | "node_modules/fake-xml-http-request/fake_xml_http_request.js", 25 | "node_modules/route-recognizer/dist/route-recognizer.js", 26 | "dist/pretender.js", 27 | ] 28 | : ["dist/pretender.bundle.js"] 29 | ).concat([ 30 | "node_modules/es6-promise/dist/es6-promise.auto.js", 31 | "node_modules/abortcontroller-polyfill/dist/abortcontroller-polyfill-only.js", 32 | "test/**/*.js", 33 | ]), 34 | 35 | // preprocess matching files before serving them to the browser 36 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 37 | preprocessors: { 38 | "dist/pretender.js": ["coverage"], 39 | }, 40 | 41 | coverageReporter: { 42 | type: "lcov", 43 | dir: "coverage", 44 | }, 45 | 46 | // test results reporter to use 47 | // possible values: 'dots', 'progress' 48 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 49 | reporters: ["dots", "coverage"], 50 | 51 | // web server port 52 | port: 9876, 53 | 54 | // enable / disable colors in the output (reporters and logs) 55 | colors: true, 56 | 57 | // level of logging 58 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 59 | logLevel: config.LOG_INFO, 60 | 61 | // enable / disable watching file and executing tests whenever any file changes 62 | autoWatch: true, 63 | 64 | // start these browsers 65 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 66 | browsers: ["Chrome", "ChromeHeadless", "ChromeHeadlessNoSandbox"], 67 | 68 | // you can define custom flags 69 | customLaunchers: { 70 | ChromeHeadlessNoSandbox: { 71 | base: "ChromeHeadless", 72 | flags: ["--no-sandbox"], 73 | }, 74 | }, 75 | 76 | // Continuous Integration mode 77 | // if true, Karma captures browsers, runs the tests and exits 78 | singleRun: false, 79 | }); 80 | }; 81 | -------------------------------------------------------------------------------- /src/interceptor.ts: -------------------------------------------------------------------------------- 1 | import FakeXMLHttpRequest from 'fake-xml-http-request'; 2 | import { createPassthrough } from './create-passthrough'; 3 | 4 | export function interceptor(ctx) { 5 | function FakeRequest() { 6 | // super() 7 | FakeXMLHttpRequest.call(this); 8 | } 9 | FakeRequest.prototype = Object.create(FakeXMLHttpRequest.prototype); 10 | FakeRequest.prototype.constructor = FakeRequest; 11 | 12 | // extend 13 | FakeRequest.prototype.send = function send() { 14 | this.sendArguments = arguments; 15 | if (!ctx.pretender.running) { 16 | throw new Error('You shut down a Pretender instance while there was a pending request. ' + 17 | 'That request just tried to complete. Check to see if you accidentally shut down ' + 18 | 'a pretender earlier than you intended to'); 19 | } 20 | 21 | FakeXMLHttpRequest.prototype.send.apply(this, arguments); 22 | 23 | if (ctx.pretender.checkPassthrough(this)) { 24 | this.passthrough(); 25 | } else { 26 | ctx.pretender.handleRequest(this); 27 | } 28 | }; 29 | 30 | FakeRequest.prototype.passthrough = function passthrough() { 31 | if (!this.sendArguments) { 32 | throw new Error('You attempted to passthrough a FakeRequest that was never sent. ' + 33 | 'Call `.send()` on the original request first'); 34 | } 35 | var xhr = createPassthrough(this, ctx.pretender._nativeXMLHttpRequest); 36 | xhr.send.apply(xhr, this.sendArguments); 37 | return xhr; 38 | }; 39 | 40 | FakeRequest.prototype._passthroughCheck = function(method, args) { 41 | if (this._passthroughRequest) { 42 | return this._passthroughRequest[method].apply(this._passthroughRequest, args); 43 | } 44 | return FakeXMLHttpRequest.prototype[method].apply(this, args); 45 | }; 46 | 47 | FakeRequest.prototype.abort = function abort() { 48 | return this._passthroughCheck('abort', arguments); 49 | }; 50 | 51 | FakeRequest.prototype.getResponseHeader = function getResponseHeader() { 52 | return this._passthroughCheck('getResponseHeader', arguments); 53 | }; 54 | 55 | FakeRequest.prototype.getAllResponseHeaders = function getAllResponseHeaders() { 56 | return this._passthroughCheck('getAllResponseHeaders', arguments); 57 | }; 58 | 59 | if (ctx.pretender._nativeXMLHttpRequest.prototype._passthroughCheck) { 60 | // eslint-disable-next-line no-console 61 | console.warn('You created a second Pretender instance while there was already one running. ' + 62 | 'Running two Pretender servers at once will lead to unexpected results and will ' + 63 | 'be removed entirely in a future major version.' + 64 | 'Please call .shutdown() on your instances when you no longer need them to respond.'); 65 | } 66 | return FakeRequest; 67 | } 68 | -------------------------------------------------------------------------------- /test/requests_stored_test.js: -------------------------------------------------------------------------------- 1 | var describe = QUnit.module; 2 | var it = QUnit.test; 3 | 4 | describe('pretender', function(config) { 5 | config.beforeEach(function() { 6 | this.pretender = new Pretender({ trackRequests: false }); 7 | }); 8 | 9 | config.afterEach(function() { 10 | this.pretender.shutdown(); 11 | }); 12 | 13 | it('does not track handled requests', function(assert) { 14 | var wasCalled; 15 | 16 | this.pretender.get('/some/path', function() { 17 | wasCalled = true; 18 | }); 19 | 20 | $.ajax({ url: '/some/path' }); 21 | 22 | assert.ok(wasCalled); 23 | assert.equal(this.pretender.handledRequests.length, 0); 24 | assert.equal(this.pretender.unhandledRequests.length, 0); 25 | assert.equal(this.pretender.passthroughRequests.length, 0); 26 | }); 27 | 28 | it('does not track unhandled requests requests', function(assert) { 29 | var wasCalled; 30 | 31 | this.pretender.get('/some/path', function() { 32 | wasCalled = true; 33 | }); 34 | 35 | $.ajax({ url: '/very/good' }); 36 | 37 | assert.notOk(wasCalled); 38 | assert.equal(this.pretender.handledRequests.length, 0); 39 | assert.equal(this.pretender.unhandledRequests.length, 0); 40 | assert.equal(this.pretender.passthroughRequests.length, 0); 41 | }); 42 | 43 | it('does not track passthrough requests requests', function(assert) { 44 | var wasCalled; 45 | 46 | this.pretender.passthrough = function() { 47 | wasCalled = true; 48 | }; 49 | 50 | this.pretender.get('/some/path', this.pretender.passthrough); 51 | 52 | $.ajax({ url: '/some/path' }); 53 | 54 | assert.ok(wasCalled); 55 | assert.equal(this.pretender.handledRequests.length, 0); 56 | assert.equal(this.pretender.unhandledRequests.length, 0); 57 | assert.equal(this.pretender.passthroughRequests.length, 0); 58 | }); 59 | }); 60 | 61 | describe('pretender', function(config) { 62 | config.beforeEach(function() { 63 | this.pretender = new Pretender(); 64 | }); 65 | 66 | config.afterEach(function() { 67 | this.pretender.shutdown(); 68 | }); 69 | 70 | it('tracks handled requests', function(assert) { 71 | var wasCalled; 72 | 73 | this.pretender.get('/some/path', function() { 74 | wasCalled = true; 75 | }); 76 | 77 | $.ajax({ url: '/some/path' }); 78 | 79 | assert.ok(wasCalled); 80 | assert.equal(this.pretender.handledRequests.length, 1); 81 | assert.equal(this.pretender.unhandledRequests.length, 0); 82 | assert.equal(this.pretender.passthroughRequests.length, 0); 83 | }); 84 | 85 | it('tracks unhandled requests requests', function(assert) { 86 | var wasCalled; 87 | 88 | this.pretender.get('/some/path', function() { 89 | wasCalled = true; 90 | }); 91 | 92 | $.ajax({ url: '/very/good' }); 93 | 94 | assert.notOk(wasCalled); 95 | assert.equal(this.pretender.handledRequests.length, 0); 96 | assert.equal(this.pretender.unhandledRequests.length, 1); 97 | assert.equal(this.pretender.passthroughRequests.length, 0); 98 | }); 99 | 100 | it('tracks passthrough requests requests', function(assert) { 101 | var wasCalled; 102 | 103 | this.pretender.passthroughRequest = function() { 104 | wasCalled = true; 105 | }; 106 | 107 | this.pretender.get('/some/path', this.pretender.passthrough); 108 | 109 | $.ajax({ url: '/some/path' }); 110 | 111 | assert.ok(wasCalled); 112 | assert.equal(this.pretender.handledRequests.length, 0); 113 | assert.equal(this.pretender.unhandledRequests.length, 0); 114 | assert.equal(this.pretender.passthroughRequests.length, 1); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /src/create-passthrough.ts: -------------------------------------------------------------------------------- 1 | export function createPassthrough(fakeXHR, nativeXMLHttpRequest) { 2 | // event types to handle on the xhr 3 | var evts = ['error', 'timeout', 'abort', 'readystatechange']; 4 | 5 | // event types to handle on the xhr.upload 6 | var uploadEvents = []; 7 | 8 | // properties to copy from the native xhr to fake xhr 9 | var lifecycleProps = [ 10 | 'readyState', 11 | 'responseText', 12 | 'response', 13 | 'responseXML', 14 | 'responseURL', 15 | 'status', 16 | 'statusText', 17 | ]; 18 | 19 | var xhr = (fakeXHR._passthroughRequest = new nativeXMLHttpRequest()); 20 | xhr.open( 21 | fakeXHR.method, 22 | fakeXHR.url, 23 | fakeXHR.async, 24 | fakeXHR.username, 25 | fakeXHR.password 26 | ); 27 | 28 | if (fakeXHR.responseType === 'arraybuffer') { 29 | lifecycleProps = ['readyState', 'response', 'status', 'statusText']; 30 | xhr.responseType = fakeXHR.responseType; 31 | } 32 | 33 | // use onload if the browser supports it 34 | if ('onload' in xhr) { 35 | evts.push('load'); 36 | } 37 | 38 | // add progress event for async calls 39 | // avoid using progress events for sync calls, they will hang https://bugs.webkit.org/show_bug.cgi?id=40996. 40 | if (fakeXHR.async && fakeXHR.responseType !== 'arraybuffer') { 41 | evts.push('progress'); 42 | uploadEvents.push('progress'); 43 | } 44 | 45 | // update `propertyNames` properties from `fromXHR` to `toXHR` 46 | function copyLifecycleProperties(propertyNames, fromXHR, toXHR) { 47 | for (var i = 0; i < propertyNames.length; i++) { 48 | var prop = propertyNames[i]; 49 | if (prop in fromXHR) { 50 | toXHR[prop] = fromXHR[prop]; 51 | } 52 | } 53 | } 54 | 55 | // fire fake event on `eventable` 56 | function dispatchEvent(eventable, eventType, event) { 57 | eventable.dispatchEvent(event); 58 | if (eventable['on' + eventType]) { 59 | eventable['on' + eventType](event); 60 | } 61 | } 62 | 63 | // set the on- handler on the native xhr for the given eventType 64 | function createHandler(eventType) { 65 | xhr['on' + eventType] = function (event) { 66 | copyLifecycleProperties(lifecycleProps, xhr, fakeXHR); 67 | dispatchEvent(fakeXHR, eventType, event); 68 | }; 69 | } 70 | 71 | // set the on- handler on the native xhr's `upload` property for 72 | // the given eventType 73 | function createUploadHandler(eventType) { 74 | if (xhr.upload && fakeXHR.upload && fakeXHR.upload['on' + eventType]) { 75 | xhr.upload['on' + eventType] = function (event) { 76 | dispatchEvent(fakeXHR.upload, eventType, event); 77 | }; 78 | } 79 | } 80 | 81 | var i; 82 | for (i = 0; i < evts.length; i++) { 83 | createHandler(evts[i]); 84 | } 85 | for (i = 0; i < uploadEvents.length; i++) { 86 | createUploadHandler(uploadEvents[i]); 87 | } 88 | 89 | if (fakeXHR.async) { 90 | xhr.timeout = fakeXHR.timeout; 91 | xhr.withCredentials = fakeXHR.withCredentials; 92 | } 93 | // XMLHttpRequest.timeout default initializes to 0, and is not allowed to be used for 94 | // synchronous XMLHttpRequests requests in a document environment. However, when a XHR 95 | // polyfill does not sets the timeout value, it will throw in React Native environment. 96 | // TODO: 97 | // synchronous XHR is deprecated, make async the default as XMLHttpRequest.open(), 98 | // and throw error if sync XHR has timeout not 0 99 | if (!xhr.timeout && xhr.timeout !== 0) { 100 | xhr.timeout = 0; // default XMLHttpRequest timeout 101 | } 102 | for (var h in fakeXHR.requestHeaders) { 103 | xhr.setRequestHeader(h, fakeXHR.requestHeaders[h]); 104 | } 105 | return xhr; 106 | } 107 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | // "lib": [], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | "declaration": false, /* Generates corresponding '.d.ts' file. */ 10 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 11 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 12 | // "outFile": "./", /* Concatenate and emit output to single file. */ 13 | // "outDir": "./", /* Redirect output structure to the directory. */ 14 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 15 | // "composite": true, /* Enable project compilation */ 16 | // "removeComments": true, /* Do not emit comments to output. */ 17 | "noEmit": true, /* Do not emit outputs. */ 18 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 19 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 20 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 21 | 22 | "newLine": "LF", 23 | 24 | /* Strict Type-Checking Options */ 25 | "strict": true, /* Enable all strict type-checking options. */ 26 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ 27 | // "strictNullChecks": true, /* Enable strict null checks. */ 28 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 29 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 30 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 31 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 32 | 33 | /* Additional Checks */ 34 | "noUnusedLocals": true, /* Report errors on unused locals. */ 35 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 36 | "noImplicitReturns": false, /* Report error when not all code paths in function return a value. */ 37 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 38 | 39 | /* Module Resolution Options */ 40 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 41 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 42 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 44 | // "typeRoots": [], /* List of folders to include type definitions from. */ 45 | // "types": [], /* Type declaration files to be included in compilation. */ 46 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 47 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 48 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 49 | 50 | /* Source Map Options */ 51 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 52 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 54 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 55 | 56 | /* Experimental Options */ 57 | // "experimentalDecorators": false, /* Enables experimental support for ES7 decorators. */ 58 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 59 | } 60 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v3.4.7 (2021-08-04) 2 | 3 | #### :bug: Bug Fix 4 | * [#331](https://github.com/pretenderjs/pretender/pull/331) Fix setting timeout to sync XHR ([@xg-wang](https://github.com/xg-wang)) 5 | 6 | #### Committers: 1 7 | - Thomas Wang ([@xg-wang](https://github.com/xg-wang)) 8 | 9 | 10 | ## v3.4.6 (2021-07-30) 11 | 12 | #### :bug: Bug Fix 13 | * [#322](https://github.com/pretenderjs/pretender/pull/322) Closes [#321](https://github.com/pretenderjs/pretender/issues/321): improve ResponseHandler types ([@mrijke](https://github.com/mrijke)) 14 | 15 | #### Committers: 1 16 | - Maarten Rijke ([@mrijke](https://github.com/mrijke)) 17 | 18 | 19 | ## v3.4.5 (2021-07-27) 20 | 21 | #### :house: Internal 22 | * [#328](https://github.com/pretenderjs/pretender/pull/328) Upgrade runtime deps ([@xg-wang](https://github.com/xg-wang)) 23 | 24 | #### Committers: 1 25 | - Thomas Wang ([@xg-wang](https://github.com/xg-wang)) 26 | 27 | 28 | ## v3.4.4 (2021-07-27) 29 | 30 | #### :bug: Bug Fix 31 | * [#326](https://github.com/pretenderjs/pretender/pull/326) Fix - Add default timeout to XMLHttpRequest ([@bombillazo](https://github.com/bombillazo)) 32 | * [#320](https://github.com/pretenderjs/pretender/pull/320) Handle progress events for FormData request body properly ([@mrijke](https://github.com/mrijke)) 33 | 34 | #### :memo: Documentation 35 | * [#315](https://github.com/pretenderjs/pretender/pull/315) README update ([@ledleds](https://github.com/ledleds)) 36 | 37 | #### :house: Internal 38 | * [#317](https://github.com/pretenderjs/pretender/pull/317) Rewrite pretender to typescript ([@xg-wang](https://github.com/xg-wang)) 39 | 40 | #### Committers: 5 41 | - Hector Ayala ([@bombillazo](https://github.com/bombillazo)) 42 | - Maarten Rijke ([@mrijke](https://github.com/mrijke)) 43 | - Robert Jackson ([@rwjblue](https://github.com/rwjblue)) 44 | - Thomas Wang ([@xg-wang](https://github.com/xg-wang)) 45 | - Vicky Ledsom ([@ledleds](https://github.com/ledleds)) 46 | 47 | 48 | ## v3.4.3 (2020-05-19) 49 | 50 | #### :memo: Documentation 51 | * [#302](https://github.com/pretenderjs/pretender/pull/302) Publish pretender.bundle.js bundling dependencies ([@xg-wang](https://github.com/xg-wang)) 52 | 53 | #### Committers: 1 54 | - Thomas Wang ([@xg-wang](https://github.com/xg-wang)) 55 | 56 | 57 | ## v3.4.2 (2020-05-19) 58 | 59 | #### :bug: Bug Fix 60 | * [#304](https://github.com/pretenderjs/pretender/pull/304) Don't attach upload handler if they do not exist on the originating request ([@ryanto](https://github.com/ryanto)) 61 | 62 | #### Committers: 2 63 | - Ryan T ([@ryanto](https://github.com/ryanto)) 64 | 65 | 66 | ## v3.4.1 (2020-04-20) 67 | 68 | #### :bug: Bug Fix 69 | * [#299](https://github.com/pretenderjs/pretender/pull/299) fix: passThroughRequest typo ([@mattrothenberg](https://github.com/mattrothenberg)) 70 | 71 | #### :house: Internal 72 | * [#257](https://github.com/pretenderjs/pretender/pull/257) refactor: extract create-passthrough & interceptor ([@givanse](https://github.com/givanse)) 73 | * [#256](https://github.com/pretenderjs/pretender/pull/256) refactor: extract hosts ([@givanse](https://github.com/givanse)) 74 | 75 | #### Committers: 2 76 | - Gastón I. Silva ([@givanse](https://github.com/givanse)) 77 | - Matt Rothenberg ([@mattrothenberg](https://github.com/mattrothenberg)) 78 | 79 | ## v3.4.0 (2020-03-21) 80 | 81 | #### :rocket: Enhancement 82 | * [#296](https://github.com/pretenderjs/pretender/pull/296) Vendor whatwg-fetch using rollup+devDep ([@ryanto](https://github.com/ryanto)) 83 | 84 | #### :house: Internal 85 | * [#294](https://github.com/pretenderjs/pretender/pull/294) Upgrade devDependencies and CI ([@xg-wang](https://github.com/xg-wang)) 86 | 87 | #### Committers: 2 88 | - Ryan T ([@ryanto](https://github.com/ryanto)) 89 | - Thomas Wang ([@xg-wang](https://github.com/xg-wang)) 90 | 91 | ## v3.3.1 (2020-02-09) 92 | 93 | #### :bug: Bug Fix 94 | * [#291](https://github.com/pretenderjs/pretender/pull/291) Fix typo in type defs ([@mfeckie](https://github.com/mfeckie)) 95 | * [#290](https://github.com/pretenderjs/pretender/pull/290) Removes unnecessary url-parse required from iife-wrapper ([@eluciano11](https://github.com/eluciano11)) 96 | 97 | #### Committers: 2 98 | - Emmanuel Luciano ([@eluciano11](https://github.com/eluciano11)) 99 | - Martin Feckie ([@mfeckie](https://github.com/mfeckie)) 100 | 101 | ## v3.3.0 (2020-02-07) 102 | 103 | #### :rocket: Enhancement 104 | * [#288](https://github.com/pretenderjs/pretender/pull/288) Adds url parse and removes the usage of anchor ([@eluciano11](https://github.com/eluciano11)) 105 | 106 | #### Committers: 2 107 | - Emmanuel Luciano ([@eluciano11](https://github.com/eluciano11)) 108 | - Ryan Stelly ([@FLGMwt](https://github.com/FLGMwt)) 109 | 110 | ## 3.2.0 (2019-12-22) 111 | 112 | #### :rocket: Enhancement 113 | * [#238](https://github.com/pretenderjs/pretender/pull/238) Expose a way to passthrough after the request is found to be unhandled ([@happycollision](https://github.com/happycollision)) 114 | 115 | #### Committers: 1 116 | - Don Denton ([@happycollision](https://github.com/happycollision)) 117 | 118 | ## 3.1.0 (2019-11-28) 119 | 120 | #### :rocket: Enhancement 121 | * [#280](https://github.com/pretenderjs/pretender/pull/280) support FakeXMLHttpRequest response property ([@kellyselden](https://github.com/kellyselden)) 122 | 123 | #### Committers: 1 124 | - Kelly Selden ([@kellyselden](https://github.com/kellyselden)) 125 | 126 | ## 3.0.4 (2019-11-04) 127 | 128 | #### :bug: Bug Fix 129 | * [#279](https://github.com/pretenderjs/pretender/pull/279) fix: remove node engine constraint ([@xg-wang](https://github.com/xg-wang)) 130 | 131 | #### Committers: 1 132 | - Thomas Wang ([@xg-wang](https://github.com/xg-wang)) 133 | 134 | ## 3.0.3 (2019-11-03) 135 | 136 | #### :bug: Bug Fix 137 | * [#278](https://github.com/pretenderjs/pretender/pull/278) Add missing type definitions in index.d.ts ([@ohcibi](https://github.com/ohcibi)) 138 | * [#276](https://github.com/pretenderjs/pretender/pull/276) fix: progress event expects byte size not time ([@xg-wang](https://github.com/xg-wang)) 139 | 140 | #### Committers: 2 141 | - Thomas Wang ([@xg-wang](https://github.com/xg-wang)) 142 | - Tobias Witt ([@ohcibi](https://github.com/ohcibi)) 143 | 144 | ## 3.0.2 145 | 146 | #### :bug: Bug Fix 147 | * [262](https://github.com/pretenderjs/pretender/pull/262) Fix ResponseHandler args type 148 | * [264](https://github.com/pretenderjs/pretender/pull/264) Fix missing responseURL property 149 | * [265](https://github.com/pretenderjs/pretender/pull/265) Fix passthrough type 150 | 151 | ## 3.0.1 152 | 153 | #### :boom: Breaking Change 154 | * [241](https://github.com/pretenderjs/pretender/pull/241) Drop Node 4 and 5; add Node 10, 11 155 | 156 | #### :rocket: Enhancement 157 | * [243](https://github.com/pretenderjs/pretender/pull/243) Add build step and TS support. Pretender now exports both iife and es module. 158 | * [235](https://github.com/pretenderjs/pretender/pull/235) Switch back to offical whatwg-fetch 159 | * [234](https://github.com/pretenderjs/pretender/pull/234) Enable Abortable fetch 160 | 161 | #### :bug: Bug Fix 162 | * [255](https://github.com/pretenderjs/pretender/pull/255) iife is 100% backwards compatible 163 | * [254](https://github.com/pretenderjs/pretender/pull/254) Type changes, Allow RequestHandler async param be number, this.passthrough 164 | 165 | ## 2.1.1 166 | * cleanup readme and package.json 167 | 168 | ## 2.1 169 | * [230](https://github.com/pretenderjs/pretender/pull/230) Support `fetch` 170 | 171 | ## 2.0 172 | * **Breaking change**: updated [fake-xml-http-request](https://github.com/pretenderjs/FakeXMLHttpRequest) to 2.0 (dropping support for end-of-life node versions) 173 | * Improved webpack compatiblity through using module defaults [216](https://github.com/pretenderjs/pretender/pull/216) 174 | * Added TypeScript type information [223](https://github.com/pretenderjs/pretender/pull/223) 175 | 176 | ## 1.4.1 177 | * [188](https://github.com/pretenderjs/pretender/pull/178) Console warn if a second pretender instance is started 178 | 179 | ## 1.4.0 180 | * [178](https://github.com/pretenderjs/pretender/pull/178) Warn if a second pretender instance is started 181 | * [181](https://github.com/pretenderjs/pretender/pull/181) Remove test support for node 0.12 182 | * [171](https://github.com/pretenderjs/pretender/pull/171) Fix url behavior in IE 11 183 | * [177](https://github.com/pretenderjs/pretender/pull/177) Allow handlers to return a Promise 184 | 185 | ## 1.3.0 186 | * [168](https://github.com/pretenderjs/pretender/pull/168) "Verb" methods now return handler 187 | * [166](https://github.com/pretenderjs/pretender/pull/166) HTTP `options` request type added 188 | 189 | ## 1.2.0 190 | * [#163](https://github.com/pretenderjs/pretender/pull/163) Update the dependency on route-recognizer 191 | 192 | ## 1.1 193 | * [#150](https://github.com/pretenderjs/pretender/pull/150) Update the dependency on FakeXMLHttpRequest 194 | 195 | ## 1.0 196 | * No changes. Just making the API stable. 197 | 198 | ## 0.13.0 199 | * [#148](https://github.com/pretenderjs/pretender/pull/148) Support `ArrayBuffer` responses. 200 | 201 | ## 0.12.0 202 | * [#134](https://github.com/pretenderjs/pretender/pull/134) `prepareBody` now receives the headers as a second argument, in case you want to handle something like `Content-Type` 203 | 204 | ## 0.11.0 205 | 206 | * [#137](https://github.com/pretenderjs/pretender/pull/137) Bump FakeXMLHttpRequest version to 1.3.0 to fix "event is undefined" bug 207 | * [#130](https://github.com/pretenderjs/pretender/pull/130) Fix readystatechange 208 | * [#127](https://github.com/pretenderjs/pretender/pull/127) Fix repository URL in package.json 209 | * [#120](https://github.com/pretenderjs/pretender/pull/120) Moves comment to a more appropriate location 210 | * [#119](https://github.com/pretenderjs/pretender/pull/119) Fire progress event on xhr.upload in passthrough 211 | 212 | ## 0.10.1 213 | 214 | * [#118](https://github.com/pretenderjs/pretender/pull/118) bump FakeXMLHttpRequest dependency to ~1.2.1 215 | * [#117](https://github.com/pretenderjs/pretender/pull/117) ensure xhr callbacks added with `addEventListener` fire on "passthrough"-ed requests 216 | -------------------------------------------------------------------------------- /src/pretender.ts: -------------------------------------------------------------------------------- 1 | import * as FakeFetch from 'whatwg-fetch'; 2 | import FakeXMLHttpRequest from 'fake-xml-http-request'; 3 | import { Params, QueryParams } from 'route-recognizer'; 4 | import { ResponseHandler, ResponseHandlerInstance } from '../index.d'; 5 | import Hosts from './hosts'; 6 | import parseURL from './parse-url'; 7 | import Registry from './registry'; 8 | import { interceptor } from './interceptor'; 9 | 10 | interface ExtraRequestData { 11 | url: string; 12 | method: string; 13 | params: Params; 14 | queryParams: QueryParams; 15 | } 16 | type FakeRequest = FakeXMLHttpRequest & ExtraRequestData; 17 | 18 | type Verb = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'; 19 | 20 | class NoopArray { 21 | length = 0; 22 | push(..._items: any[]) { 23 | return 0; 24 | } 25 | } 26 | 27 | function scheduleProgressEvent(request, startTime, totalTime) { 28 | let totalSize = 0; 29 | const body = request.requestBody; 30 | if (body) { 31 | if (body instanceof FormData) { 32 | body.forEach((value) => { 33 | if (value instanceof File) { 34 | totalSize += value.size; 35 | } else { 36 | totalSize += value.length; 37 | } 38 | }); 39 | } else { 40 | // Support Blob, BufferSource, USVString, ArrayBufferView 41 | totalSize = body.byteLength || body.size || body.length || 0; 42 | } 43 | } 44 | setTimeout(function () { 45 | if (!request.aborted && !request.status) { 46 | let elapsedTime = new Date().getTime() - startTime.getTime(); 47 | let progressTransmitted = 48 | totalTime <= 0 ? 0 : (elapsedTime / totalTime) * totalSize; 49 | // ProgressEvent expects loaded, total 50 | // https://xhr.spec.whatwg.org/#interface-progressevent 51 | request.upload._progress(true, progressTransmitted, totalSize); 52 | request._progress(true, progressTransmitted, totalSize); 53 | scheduleProgressEvent(request, startTime, totalTime); 54 | } else if (request.status) { 55 | // we're done, send a final progress event with loaded === total 56 | request.upload._progress(true, totalSize, totalSize); 57 | request._progress(true, totalSize, totalSize); 58 | } 59 | }, 50); 60 | } 61 | 62 | function isArray(array) { 63 | return Object.prototype.toString.call(array) === '[object Array]'; 64 | } 65 | 66 | const PASSTHROUGH = {}; 67 | 68 | function verbify(verb: Verb) { 69 | return function ( 70 | this: Pretender, 71 | path: string, 72 | handler: ResponseHandler, 73 | async: boolean 74 | ) { 75 | return this.register(verb, path, handler, async); 76 | }; 77 | } 78 | 79 | export default class Pretender { 80 | static parseURL = parseURL; 81 | static Hosts = Hosts; 82 | static Registry = Registry; 83 | 84 | hosts = new Hosts(); 85 | handlers: ResponseHandler[] = []; 86 | handledRequests: any[] | NoopArray; 87 | passthroughRequests: any[] | NoopArray; 88 | unhandledRequests: any[] | NoopArray; 89 | requestReferences: any[]; 90 | forcePassthrough: boolean; 91 | disableUnhandled: boolean; 92 | 93 | ctx: { pretender?: Pretender }; 94 | running: boolean; 95 | 96 | private _nativeXMLHttpRequest: any; 97 | private _fetchProps: string[]; 98 | 99 | constructor() { 100 | let lastArg = arguments[arguments.length - 1]; 101 | let options = typeof lastArg === 'object' ? lastArg : null; 102 | let shouldNotTrack = options && options.trackRequests === false; 103 | 104 | this.handledRequests = shouldNotTrack ? new NoopArray() : []; 105 | this.passthroughRequests = shouldNotTrack ? new NoopArray() : []; 106 | this.unhandledRequests = shouldNotTrack ? new NoopArray() : []; 107 | this.requestReferences = []; 108 | this.forcePassthrough = options && options.forcePassthrough === true; 109 | this.disableUnhandled = options && options.disableUnhandled === true; 110 | 111 | // reference the native XMLHttpRequest object so 112 | // it can be restored later 113 | this._nativeXMLHttpRequest = (self).XMLHttpRequest; 114 | this.running = false; 115 | let ctx = { pretender: this }; 116 | this.ctx = ctx; 117 | 118 | // capture xhr requests, channeling them into 119 | // the route map. 120 | (self).XMLHttpRequest = interceptor(ctx); 121 | 122 | // polyfill fetch when xhr is ready 123 | this._fetchProps = FakeFetch 124 | ? ['fetch', 'Headers', 'Request', 'Response'] 125 | : []; 126 | this._fetchProps.forEach((name) => { 127 | (this)['_native' + name] = self[name]; 128 | self[name] = FakeFetch[name]; 129 | }, this); 130 | 131 | // 'start' the server 132 | this.running = true; 133 | 134 | // trigger the route map DSL. 135 | let argLength = options ? arguments.length - 1 : arguments.length; 136 | for (let i = 0; i < argLength; i++) { 137 | this.map(arguments[i]); 138 | } 139 | } 140 | 141 | get = verbify('GET'); 142 | post = verbify('POST'); 143 | put = verbify('PUT'); 144 | delete = verbify('DELETE'); 145 | patch = verbify('PATCH'); 146 | head = verbify('HEAD'); 147 | options = verbify('OPTIONS'); 148 | 149 | map(maps: (pretender: Pretender) => void) { 150 | maps.call(this); 151 | } 152 | 153 | register( 154 | verb: string, 155 | url: string, 156 | handler: ResponseHandler, 157 | async: boolean 158 | ): ResponseHandlerInstance { 159 | if (!handler) { 160 | throw new Error( 161 | 'The function you tried passing to Pretender to handle ' + 162 | verb + 163 | ' ' + 164 | url + 165 | ' is undefined or missing.' 166 | ); 167 | } 168 | 169 | const handlerInstance = handler as ResponseHandlerInstance; 170 | 171 | handlerInstance.numberOfCalls = 0; 172 | handlerInstance.async = async; 173 | this.handlers.push(handlerInstance); 174 | 175 | let registry = this.hosts.forURL(url)[verb]; 176 | 177 | registry.add([ 178 | { 179 | path: parseURL(url).fullpath, 180 | handler: handlerInstance, 181 | }, 182 | ]); 183 | 184 | return handlerInstance; 185 | } 186 | 187 | passthrough = PASSTHROUGH; 188 | 189 | checkPassthrough(request: FakeRequest) { 190 | let verb = request.method.toUpperCase() as Verb; 191 | let path = parseURL(request.url).fullpath; 192 | let recognized = this.hosts.forURL(request.url)[verb].recognize(path); 193 | let match = recognized && recognized[0]; 194 | 195 | if ((match && match.handler === PASSTHROUGH) || this.forcePassthrough) { 196 | this.passthroughRequests.push(request); 197 | this.passthroughRequest(verb, path, request); 198 | return true; 199 | } 200 | 201 | return false; 202 | } 203 | 204 | handleRequest(request: FakeRequest) { 205 | let verb = request.method.toUpperCase(); 206 | let path = request.url; 207 | 208 | let handler = this._handlerFor(verb, path, request); 209 | 210 | if (handler) { 211 | handler.handler.numberOfCalls++; 212 | let async = handler.handler.async; 213 | this.handledRequests.push(request); 214 | 215 | let pretender = this; 216 | 217 | let _handleRequest = function (statusHeadersAndBody) { 218 | if (!isArray(statusHeadersAndBody)) { 219 | let note = 220 | 'Remember to `return [status, headers, body];` in your route handler.'; 221 | throw new Error( 222 | 'Nothing returned by handler for ' + path + '. ' + note 223 | ); 224 | } 225 | 226 | let status = statusHeadersAndBody[0]; 227 | let headers = pretender.prepareHeaders(statusHeadersAndBody[1]); 228 | let body = pretender.prepareBody(statusHeadersAndBody[2], headers); 229 | 230 | pretender.handleResponse(request, async, function () { 231 | request.respond(status, headers, body); 232 | pretender.handledRequest(verb, path, request); 233 | }); 234 | }; 235 | 236 | try { 237 | let result = handler.handler(request); 238 | if (result && typeof result.then === 'function') { 239 | // `result` is a promise, resolve it 240 | result.then(function (resolvedResult) { 241 | _handleRequest(resolvedResult); 242 | }); 243 | } else { 244 | _handleRequest(result); 245 | } 246 | } catch (error) { 247 | this.erroredRequest(verb, path, request, error); 248 | this.resolve(request); 249 | } 250 | } else { 251 | if (!this.disableUnhandled) { 252 | this.unhandledRequests.push(request); 253 | this.unhandledRequest(verb, path, request); 254 | } 255 | } 256 | } 257 | 258 | handleResponse(request: FakeRequest, strategy, callback: Function) { 259 | let delay = typeof strategy === 'function' ? strategy() : strategy; 260 | delay = typeof delay === 'boolean' || typeof delay === 'number' ? delay : 0; 261 | 262 | if (delay === false) { 263 | callback(); 264 | } else { 265 | let pretender = this; 266 | pretender.requestReferences.push({ 267 | request: request, 268 | callback: callback, 269 | }); 270 | 271 | if (delay !== true) { 272 | scheduleProgressEvent(request, new Date(), delay); 273 | setTimeout(function () { 274 | pretender.resolve(request); 275 | }, delay); 276 | } 277 | } 278 | } 279 | 280 | resolve(request: FakeRequest) { 281 | for (let i = 0, len = this.requestReferences.length; i < len; i++) { 282 | let res = this.requestReferences[i]; 283 | if (res.request === request) { 284 | res.callback(); 285 | this.requestReferences.splice(i, 1); 286 | break; 287 | } 288 | } 289 | } 290 | 291 | requiresManualResolution(verb: string, path: string) { 292 | let handler = this._handlerFor(verb.toUpperCase(), path, {}); 293 | if (!handler) { 294 | return false; 295 | } 296 | 297 | let async = handler.handler.async; 298 | return typeof async === 'function' ? async() === true : async === true; 299 | } 300 | prepareBody(body, _headers) { 301 | return body; 302 | } 303 | prepareHeaders(headers) { 304 | return headers; 305 | } 306 | handledRequest(_verb, _path, _request) { 307 | /* no-op */ 308 | } 309 | passthroughRequest(_verb, _path, _request) { 310 | /* no-op */ 311 | } 312 | unhandledRequest(verb, path, _request) { 313 | throw new Error( 314 | 'Pretender intercepted ' + 315 | verb + 316 | ' ' + 317 | path + 318 | ' but no handler was defined for this type of request' 319 | ); 320 | } 321 | erroredRequest(verb, path, _request, error) { 322 | error.message = 323 | 'Pretender intercepted ' + 324 | verb + 325 | ' ' + 326 | path + 327 | ' but encountered an error: ' + 328 | error.message; 329 | throw error; 330 | } 331 | shutdown() { 332 | (self).XMLHttpRequest = this._nativeXMLHttpRequest; 333 | this._fetchProps.forEach((name) => { 334 | self[name] = this['_native' + name]; 335 | }, this); 336 | this.ctx.pretender = undefined; 337 | // 'stop' the server 338 | this.running = false; 339 | } 340 | 341 | private _handlerFor(verb: Verb, url: string, request: FakeRequest) { 342 | let registry = this.hosts.forURL(url)[verb]; 343 | let matches = registry.recognize(parseURL(url).fullpath); 344 | 345 | let match = matches ? matches[0] : null; 346 | if (match) { 347 | request.params = match.params; 348 | request.queryParams = matches.queryParams; 349 | } 350 | 351 | return match; 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /test/passthrough_test.js: -------------------------------------------------------------------------------- 1 | var originalXMLHttpRequest; 2 | var describe = QUnit.module; 3 | var it = QUnit.test; 4 | 5 | describe('passthrough requests', function (config) { 6 | config.beforeEach(function () { 7 | originalXMLHttpRequest = window.XMLHttpRequest; 8 | this.pretender = new Pretender(); 9 | }); 10 | 11 | config.afterEach(function () { 12 | this.pretender.shutdown(); 13 | window.XMLHttpRequest = originalXMLHttpRequest; 14 | }); 15 | 16 | it('allows matched paths to be pass-through', function (assert) { 17 | var pretender = this.pretender; 18 | var done = assert.async(); 19 | 20 | pretender.post('/some/:route', pretender.passthrough); 21 | 22 | var passthroughInvoked = false; 23 | pretender.passthroughRequest = function (verb, path, request) { 24 | passthroughInvoked = true; 25 | assert.equal(verb, 'POST'); 26 | assert.equal(path, '/some/path'); 27 | assert.equal(request.requestBody, 'some=data'); 28 | }; 29 | 30 | $.ajax({ 31 | url: '/some/path', 32 | method: 'POST', 33 | headers: { 34 | 'test-header': 'value', 35 | }, 36 | data: { 37 | some: 'data', 38 | }, 39 | error: function (xhr) { 40 | assert.equal(xhr.status, 404); 41 | assert.ok(passthroughInvoked); 42 | done(); 43 | }, 44 | }); 45 | }); 46 | 47 | it( 48 | 'asynchronous request with pass-through has timeout,' + 49 | 'withCredentials and onprogress event', 50 | function (assert) { 51 | var pretender = this.pretender; 52 | var done = assert.async(); 53 | 54 | function testXHR() { 55 | this.pretender = pretender; 56 | this.open = function () {}; 57 | this.setRequestHeader = function () {}; 58 | this.upload = {}; 59 | this.send = { 60 | pretender: pretender, 61 | apply: function (xhr /*, argument*/) { 62 | assert.ok('timeout' in xhr); 63 | assert.ok('withCredentials' in xhr); 64 | assert.ok('onprogress' in xhr); 65 | this.pretender.resolve(xhr); 66 | done(); 67 | }, 68 | }; 69 | } 70 | pretender._nativeXMLHttpRequest = testXHR; 71 | 72 | pretender.post('/some/path', pretender.passthrough); 73 | 74 | var xhr = new window.XMLHttpRequest(); 75 | xhr.open('POST', '/some/path'); 76 | xhr.timeout = 1000; 77 | xhr.withCredentials = true; 78 | xhr.send('some data'); 79 | } 80 | ); 81 | 82 | it( 83 | 'asynchronous request with pass-through and ' + 84 | 'arraybuffer as responseType', 85 | function (assert) { 86 | var pretender = this.pretender; 87 | var done = assert.async(); 88 | 89 | function testXHR() { 90 | this.pretender = pretender; 91 | this.open = function () {}; 92 | this.setRequestHeader = function () {}; 93 | this.upload = {}; 94 | this.responseType = ''; 95 | this.send = { 96 | pretender: pretender, 97 | apply: function (xhr /*, argument*/) { 98 | assert.equal(xhr.responseType, 'arraybuffer'); 99 | this.pretender.resolve(xhr); 100 | done(); 101 | }, 102 | }; 103 | } 104 | pretender._nativeXMLHttpRequest = testXHR; 105 | 106 | pretender.get('/some/path', pretender.passthrough); 107 | 108 | var xhr = new window.XMLHttpRequest(); 109 | xhr.open('GET', '/some/path'); 110 | xhr.responseType = 'arraybuffer'; 111 | xhr.send(); 112 | } 113 | ); 114 | 115 | it('synchronous request has timeout=0, withCredentials and onprogress event', function (assert) { 116 | var pretender = this.pretender; 117 | var done = assert.async(); 118 | 119 | function testXHR() { 120 | this.open = function () {}; 121 | this.setRequestHeader = function () {}; 122 | this.upload = {}; 123 | this.send = { 124 | pretender: pretender, 125 | apply: function (xhr /*, argument*/) { 126 | assert.equal(xhr.timeout, 0); 127 | assert.ok(!('withCredentials' in xhr)); 128 | assert.ok(!('onprogress' in xhr)); 129 | this.pretender.resolve(xhr); 130 | done(); 131 | }, 132 | }; 133 | } 134 | Object.defineProperty(testXHR, 'timeout', { 135 | get() { 136 | return 0; 137 | }, 138 | set() { 139 | throw new Error( 140 | 'Timeouts cannot be set for synchronous requests made from a document.' 141 | ); 142 | }, 143 | }); 144 | pretender._nativeXMLHttpRequest = testXHR; 145 | 146 | pretender.post('/some/path', pretender.passthrough); 147 | 148 | var xhr = new window.XMLHttpRequest(); 149 | xhr.open('POST', '/some/path', false); 150 | xhr.withCredentials = true; 151 | xhr.send('some data'); 152 | }); 153 | 154 | it('synchronous request has timeout missing will set to 0', function (assert) { 155 | var pretender = this.pretender; 156 | var done = assert.async(); 157 | 158 | function testXHR() { 159 | this.open = function () {}; 160 | this.setRequestHeader = function () {}; 161 | this.upload = {}; 162 | this.send = { 163 | pretender: pretender, 164 | apply: function (xhr /*, argument*/) { 165 | assert.equal(xhr.timeout, 0); 166 | assert.ok(!('withCredentials' in xhr)); 167 | assert.ok(!('onprogress' in xhr)); 168 | this.pretender.resolve(xhr); 169 | done(); 170 | }, 171 | }; 172 | } 173 | pretender._nativeXMLHttpRequest = testXHR; 174 | 175 | pretender.post('/some/path', pretender.passthrough); 176 | 177 | var xhr = new window.XMLHttpRequest(); 178 | xhr.open('POST', '/some/path', false); 179 | xhr.withCredentials = true; 180 | xhr.send('some data'); 181 | }); 182 | 183 | it('asynchronous request fires events', function (assert) { 184 | assert.expect(6); 185 | 186 | var pretender = this.pretender; 187 | var done = assert.async(); 188 | 189 | pretender.post('/some/:route', pretender.passthrough); 190 | 191 | var onEvents = { 192 | load: false, 193 | progress: false, 194 | readystatechange: false, 195 | }; 196 | var listenerEvents = { 197 | load: false, 198 | progress: false, 199 | readystatechange: false, 200 | }; 201 | 202 | var xhr = new window.XMLHttpRequest(); 203 | xhr.open('POST', '/some/otherpath'); 204 | 205 | xhr.addEventListener('progress', function _progress() { 206 | listenerEvents.progress = true; 207 | }); 208 | 209 | xhr.onprogress = function _onprogress() { 210 | onEvents.progress = true; 211 | }; 212 | 213 | xhr.addEventListener('load', function _load() { 214 | listenerEvents.load = true; 215 | finishNext(); 216 | }); 217 | 218 | xhr.onload = function _onload() { 219 | onEvents.load = true; 220 | finishNext(); 221 | }; 222 | 223 | xhr.addEventListener('readystatechange', function _load() { 224 | if (xhr.readyState == 4) { 225 | listenerEvents.readystatechange = true; 226 | finishNext(); 227 | } 228 | }); 229 | 230 | xhr.onreadystatechange = function _onload() { 231 | if (xhr.readyState == 4) { 232 | onEvents.readystatechange = true; 233 | finishNext(); 234 | } 235 | }; 236 | 237 | xhr.send(); 238 | 239 | // call `finish` in next tick to ensure both load event handlers 240 | // have a chance to fire. 241 | function finishNext() { 242 | setTimeout(finishOnce, 1); 243 | } 244 | 245 | var finished = false; 246 | function finishOnce() { 247 | if (!finished) { 248 | finished = true; 249 | 250 | assert.ok(onEvents.load, 'onload called'); 251 | assert.ok(onEvents.progress, 'onprogress called'); 252 | assert.ok(onEvents.readystatechange, 'onreadystate called'); 253 | 254 | assert.ok(listenerEvents.load, 'load listener called'); 255 | assert.ok(listenerEvents.progress, 'progress listener called'); 256 | assert.ok( 257 | listenerEvents.readystatechange, 258 | 'readystate listener called' 259 | ); 260 | 261 | done(); 262 | } 263 | } 264 | }); 265 | 266 | it('asynchronous request fires upload progress events', function (assert) { 267 | assert.expect(2); 268 | 269 | var pretender = this.pretender; 270 | var done = assert.async(); 271 | 272 | pretender.post('/some/:route', pretender.passthrough); 273 | 274 | var onEvents = { 275 | progress: false, 276 | }; 277 | var listenerEvents = { 278 | progress: false, 279 | }; 280 | 281 | var xhr = new window.XMLHttpRequest(); 282 | xhr.open('POST', '/some/otherpath'); 283 | 284 | xhr.upload.addEventListener('progress', function _progress() { 285 | listenerEvents.progress = true; 286 | }); 287 | 288 | xhr.upload.onprogress = function _onprogress() { 289 | onEvents.progress = true; 290 | }; 291 | 292 | xhr.onload = function _onload() { 293 | setTimeout(finish, 1); 294 | }; 295 | 296 | xhr.send('some data'); 297 | 298 | // ensure the test ends 299 | var failTimer = setTimeout(function () { 300 | assert.ok(false, 'test timed out'); 301 | done(); 302 | }, 500); 303 | 304 | var finished = false; 305 | function finish() { 306 | if (!finished) { 307 | finished = true; 308 | clearTimeout(failTimer); 309 | assert.ok(onEvents.progress, 'onprogress called'); 310 | assert.ok(listenerEvents.progress, 'progress listener called'); 311 | 312 | done(); 313 | } 314 | } 315 | }); 316 | 317 | it('asynchronous request with pass-through and empty response', function (assert) { 318 | var done = assert.async(); 319 | var pretender = this.pretender; 320 | 321 | function testXHR() { 322 | this.pretender = pretender; 323 | this.open = function () {}; 324 | this.setRequestHeader = function () {}; 325 | this.responseText = ''; 326 | this.response = ''; 327 | this.onload = true; 328 | this.send = { 329 | pretender: pretender, 330 | apply: function (xhr /*, argument*/) { 331 | xhr.onload({ target: xhr, type: 'load' }); 332 | }, 333 | }; 334 | } 335 | pretender._nativeXMLHttpRequest = testXHR; 336 | 337 | pretender.get('/some/path', pretender.passthrough); 338 | 339 | var xhr = new window.XMLHttpRequest(); 340 | xhr.open('GET', '/some/path'); 341 | xhr.addEventListener('load', function _onload(event) { 342 | assert.equal( 343 | xhr.responseText, 344 | event.target.responseText, 345 | 'responseText for real and fake xhr are both blank strings' 346 | ); 347 | assert.equal( 348 | xhr.response, 349 | event.target.response, 350 | 'response for real and fake xhr are both blank strings' 351 | ); 352 | done(); 353 | }); 354 | 355 | xhr.send(); 356 | }); 357 | 358 | describe('the `.passthrough()` property', function () { 359 | it('allows a passthrough on an unhandledRequest', function (assert) { 360 | var done = assert.async(); 361 | 362 | this.pretender.unhandledRequest = function (_verb, _path, request) { 363 | request.passthrough(); 364 | }; 365 | 366 | $.ajax({ 367 | url: '/some/path', 368 | error: function (xhr) { 369 | assert.equal(xhr.status, 404); 370 | done(); 371 | }, 372 | }); 373 | }); 374 | 375 | it('returns a native xhr', function (assert) { 376 | var done = assert.async(); 377 | 378 | var pretender = this.pretender; 379 | 380 | function testXHR() { 381 | this.pretender = pretender; 382 | this.open = function () {}; 383 | this.setRequestHeader = function () {}; 384 | this.responseText = ''; 385 | this.response = ''; 386 | this.onload = true; 387 | this.send = { 388 | pretender: pretender, 389 | apply: function (xhr /*, argument*/) { 390 | xhr.onload({ target: xhr, type: 'load' }); 391 | }, 392 | }; 393 | } 394 | pretender._nativeXMLHttpRequest = testXHR; 395 | 396 | var xhr = new window.XMLHttpRequest(); 397 | xhr.open('GET', '/some/path'); 398 | 399 | this.pretender.unhandledRequest = function (_verb, _path, request) { 400 | var referencedXhr = request.passthrough(); 401 | assert.ok(referencedXhr instanceof testXHR); 402 | done(); 403 | }; 404 | 405 | xhr.send(); 406 | }); 407 | }); 408 | }); 409 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pretender 2 | 3 | [![npm version](https://badge.fury.io/js/pretender.svg)](https://badge.fury.io/js/pretender) 4 | [![Build Status](https://travis-ci.org/pretenderjs/pretender.svg)](https://travis-ci.org/pretenderjs/pretender) 5 | [![Coverage Status](https://coveralls.io/repos/pretenderjs/pretender/badge.svg?branch=master&service=github)](https://coveralls.io/github/pretenderjs/pretender?branch=master) 6 | [![Dependency Status](https://david-dm.org/pretenderjs/pretender.svg)](https://david-dm.org/pretenderjs/pretender) 7 | [![devDependency Status](https://david-dm.org/pretenderjs/pretender/dev-status.svg)](https://david-dm.org/pretenderjs/pretender#info=devDependencies) 8 | [![Code Climate](https://codeclimate.com/github/pretenderjs/pretender/badges/gpa.svg)](https://codeclimate.com/github/pretenderjs/pretender) 9 | 10 | Pretender is a mock server library for XMLHttpRequest and Fetch, that comes 11 | with an express/sinatra style syntax for defining routes and their handlers. 12 | 13 | Pretender will temporarily replace native XMLHttpRequest and Fetch , intercept 14 | all requests, and direct them to little pretend service you've defined. 15 | 16 | **:warning: Pretender only works in the browser!** 17 | 18 | ```javascript 19 | const PHOTOS = { 20 | "10": { 21 | id: 10, 22 | src: 'http://media.giphy.com/media/UdqUo8xvEcvgA/giphy.gif' 23 | }, 24 | "42": { 25 | id: 42, 26 | src: 'http://media0.giphy.com/media/Ko2pyD26RdYRi/giphy.gif' 27 | } 28 | }; 29 | 30 | const server = new Pretender(function() { 31 | this.get('/photos', request => { 32 | let all = JSON.stringify(Object.keys(PHOTOS).map(k => PHOTOS[k])); 33 | return [200, {"Content-Type": "application/json"}, all] 34 | }); 35 | 36 | this.get('/photos/:id', request => { 37 | return [200, {"Content-Type": "application/json"}, JSON.stringify(PHOTOS[request.params.id])] 38 | }); 39 | }); 40 | 41 | $.get('/photos/12', {success() => { ... }}) 42 | ``` 43 | 44 | ## Usage 45 | 46 | ``` 47 | yarn add -D pretender 48 | # or 49 | npm install --save-dev pretender 50 | ``` 51 | 52 | You can load Pretender directly in the browser. 53 | 54 | ```javascript 55 | 56 | ``` 57 | 58 | Or as a module: 59 | 60 | ```javascript 61 | import Pretender from 'pretender'; 62 | const server = new Pretender(function() {}); 63 | ``` 64 | 65 | ## The Server DSL 66 | The server DSL is inspired by express/sinatra. Pass a function to the Pretender constructor 67 | that will be invoked with the Pretender instance as its context. Available methods are 68 | `get`, `put`, `post`, `delete`, `patch`, and `head`. Each of these methods takes a path pattern, 69 | a callback, and an optional [timing parameter](#timing-parameter). The callback will be invoked with a 70 | single argument (the XMLHttpRequest instance that triggered this request) and must return an array 71 | containing the HTTP status code, headers object, and body as a string. 72 | 73 | ```javascript 74 | const server = new Pretender(function() { 75 | this.put('/api/songs/99', request => [404, {}, ""]); 76 | }); 77 | ``` 78 | 79 | a Pretender constructor can take multiple maps: 80 | 81 | ```javascript 82 | import adminMaps from "testing/maps/admin"; 83 | import photoMaps from "testing/maps/photos"; 84 | 85 | const server = new Pretender(photoMaps, adminMaps); 86 | ``` 87 | 88 | ```javascript 89 | // testing/maps/photos 90 | 91 | const PHOTOS = { 92 | "58": { 93 | id: 58, 94 | src: 'https://media.giphy.com/media/65TpAhHZ7A9nuf3GIu/giphy.gif' 95 | }, 96 | "99": { 97 | id: 99, 98 | src: 'https://media.giphy.com/media/4Zd5qAcFv759xnegdo/giphy.gif' 99 | } 100 | }; 101 | 102 | export default function() { 103 | this.get('/photos/:id', () => 104 | [200, {"Content-Type": "application/json"}, JSON.stringify(PHOTOS[request.params.id])] 105 | ); 106 | } 107 | ``` 108 | 109 | The HTTP verb methods can also be called on an instance individually: 110 | 111 | ```javascript 112 | const server = new Pretender(); 113 | server.put('/api/songs/99', request => [404, {}, ""]); 114 | ``` 115 | 116 | ### Paths 117 | Paths can either be hard-coded (`this.get('/api/songs/12')`) or contain dynamic segments 118 | (`this.get('/api/songs/:song_id'`). If there were dynamic segments of the path, 119 | these will be attached to the request object as a `params` property with keys matching 120 | the dynamic portion and values with the matching value from the path. 121 | 122 | ```javascript 123 | const server = new Pretender(function() { 124 | this.get('/api/songs/:song_id', request => request.params.song_id); 125 | }); 126 | 127 | $.get('/api/songs/871') // params.song_id will be '871' 128 | 129 | ``` 130 | 131 | ### Query Parameters 132 | If there were query parameters in the request, these will be attached to the request object as a `queryParams` 133 | property. 134 | 135 | ```javascript 136 | const server = new Pretender(function() { 137 | this.get('/api/songs', request => request.queryParams.sortOrder); 138 | }); 139 | 140 | // typical jQuery-style uses you've probably seen. 141 | // queryParams.sortOrder will be 'asc' for both styles. 142 | $.get({url: '/api/songs', data: { sortOrder: 'asc' }}); 143 | $.get('/api/songs?sortOrder=asc'); 144 | 145 | ``` 146 | 147 | 148 | ### Responding 149 | You must return an array from this handler that includes the HTTP status code, an object literal 150 | of response headers, and a string body. 151 | 152 | ```javascript 153 | const server = new Pretender(function() { 154 | this.get('/api/songs', request => { 155 | return [ 156 | 200, 157 | {'content-type': 'application/javascript'}, 158 | '[{"id": 12}, {"id": 14}]' 159 | ]; 160 | }); 161 | }); 162 | ``` 163 | 164 | Or, optionally, return a Promise. 165 | 166 | ```javascript 167 | const server = new Pretender(function() { 168 | this.get('/api/songs', request => { 169 | return new Promise(resolve => { 170 | let response = [ 171 | 200, 172 | {'content-type': 'application/javascript'}, 173 | '[{"id": 12}, {"id": 14}]' 174 | ]; 175 | 176 | resolve(response); 177 | }); 178 | }); 179 | }); 180 | ``` 181 | 182 | ### Pass-Through 183 | You can specify paths that should be ignored by pretender and made as real XHR requests. 184 | Enable these by specifying pass-through routes with `pretender.passthrough`: 185 | 186 | ```javascript 187 | const server = new Pretender(function() { 188 | this.get('/photos/:id', this.passthrough); 189 | }); 190 | ``` 191 | 192 | In some cases, you will need to force pretender to passthough, just start your server with the `forcePassthrough` option. 193 | 194 | ```javascript 195 | const server = new Pretender({ forcePassthrough: true }) 196 | ``` 197 | 198 | Other times, you may want to decide whether or not to passthrough when the call is made. In that 199 | case you can use the `.passthrough()` function on the fake request itself. (The [`unhandledRequest` 200 | property is discussed below](#unhandled-requests).) 201 | 202 | ```javascript 203 | server.unhandledRequest = function(verb, path, request) { 204 | if (myIgnoreRequestChecker(path)) { 205 | console.warn(`Ignoring request) ${verb.toUpperCase()} : ${path}`); 206 | } else { 207 | console.warn( 208 | `Unhandled ${verb.toUpperCase()} : ${path} >> Passing along. See eventual response below.` 209 | ) 210 | 211 | const xhr = request.passthrough(); // <-- A native, sent xhr is returned 212 | 213 | xhr.onloadend = (ev) => { 214 | console.warn(`Response for ${path}`, { 215 | verb, 216 | path, 217 | request, 218 | responseEvent: ev, 219 | }) 220 | }; 221 | } 222 | }; 223 | ``` 224 | 225 | The `.passthrough()` function will immediately create, send, and return a native `XMLHttpRequest`. 226 | 227 | ### Timing Parameter 228 | The timing parameter is used to control when a request responds. By default, a request responds 229 | asynchronously on the next frame of the browser's event loop. A request can also be configured to respond 230 | synchronously, after a defined amount of time, or never (i.e., it needs to be manually resolved). 231 | 232 | **Default** 233 | ```javascript 234 | const server = new Pretender(function() { 235 | // songHandler will execute the frame after receiving a request (async) 236 | this.get('/api/songs', songHandler); 237 | }); 238 | ``` 239 | 240 | **Synchronous** 241 | ```javascript 242 | const server = new Pretender(function() { 243 | // songHandler will execute immediately after receiving a request (sync) 244 | this.get('/api/songs', songHandler, false); 245 | }); 246 | ``` 247 | 248 | **Delay** 249 | ```javascript 250 | const server = new Pretender(function() { 251 | // songHandler will execute two seconds after receiving a request (async) 252 | this.get('/api/songs', songHandler, 2000); 253 | }); 254 | ``` 255 | 256 | **Manual** 257 | ```javascript 258 | const server = new Pretender(function() { 259 | // songHandler will only execute once you manually resolve the request 260 | this.get('/api/songs', songHandler, true); 261 | }); 262 | 263 | // resolve a request like this 264 | server.resolve(theXMLHttpRequestThatRequestedTheSongsRoute); 265 | ``` 266 | 267 | #### Using functions for the timing parameter 268 | You may want the timing behavior of a response to change from request to request. This can be 269 | done by providing a function as the timing parameter. 270 | 271 | ```javascript 272 | const externalState = 'idle'; 273 | 274 | function throttler() { 275 | if (externalState === 'OH NO DDOS ATTACK') { 276 | return 15000; 277 | } 278 | } 279 | 280 | const server = new Pretender(function() { 281 | // songHandler will only execute based on the result of throttler 282 | this.get('/api/songs', songHandler, throttler); 283 | }); 284 | ``` 285 | 286 | Now whenever the songs route is requested, its timing behavior will be determined by the result 287 | of the call to `throttler`. When `externalState` is idle, `throttler` returns `undefined`, which 288 | means the route will use the default behavior. 289 | 290 | When the time is right, you can set `externalState` to `"OH NO DOS ATTACK"` which will make all 291 | future requests take 15 seconds to respond. 292 | 293 | #### Scheduling ProgressEvent 294 | If the timing parameter is resolved as async, then a [`ProgressEvent`](https://xhr.spec.whatwg.org/#interface-progressevent) 295 | will be scheduled every 50ms until the request has a response or is aborted. 296 | 297 | To listen to the progress, you can define `onprogress` on the `XMLHttpRequest` object or 298 | its [`upload` attribute](https://xhr.spec.whatwg.org/#the-upload-attribute). 299 | 300 | ```javascript 301 | let xhr = new window.XMLHttpRequest(); 302 | xhr.open('POST', '/uploads'); 303 | // https://fetch.spec.whatwg.org/#concept-request-body 304 | // https://xhr.spec.whatwg.org/#the-send()-method 305 | let postBody = new ArrayBuffer(8); 306 | xhr.upload.onprogress = function(event) { 307 | // event.lengthComputable === true 308 | // event.total === 8 309 | // event.loaded will be incremented every ~50ms 310 | }; 311 | xhr.onprogress = function(event) { 312 | // xhr onprogress will also be triggered 313 | }; 314 | xhr.send(postBody); 315 | ``` 316 | 317 | ## Sharing routes 318 | You can call `map` multiple times on a Pretender instance. This is a great way to share and reuse 319 | sets of routes between tests: 320 | 321 | ```javascript 322 | export function authenticationRoutes() { 323 | this.post('/authenticate',() => { ... }); 324 | this.post('/signout', () => { ... }); 325 | } 326 | 327 | export function songsRoutes() { 328 | this.get('/api/songs',() => { ... }); 329 | } 330 | ``` 331 | 332 | 333 | ```javascript 334 | // a test 335 | 336 | import {authenticationRoutes, songsRoutes} from "../shared/routes"; 337 | import Pretender from "pretender"; 338 | 339 | let p = new Pretender(); 340 | p.map(authenticationRoutes); 341 | p.map(songsRoutes); 342 | ``` 343 | 344 | ## Hooks 345 | ### Handled Requests 346 | In addition to responding to the request, your server will call a `handledRequest` method with 347 | the HTTP `verb`, `path`, and original `request`. By default this method does nothing. You can 348 | override this method to supply your own behavior like logging or test framework integration: 349 | 350 | ```javascript 351 | const server = new Pretender(function() { 352 | this.put('/api/songs/:song_id', request => { 353 | return [202, {"Content-Type": "application/json"}, "{}"] 354 | }); 355 | }); 356 | 357 | server.handledRequest = function(verb, path, request) { 358 | console.log("a request was responded to"); 359 | } 360 | 361 | $.getJSON("/api/songs/12"); 362 | ``` 363 | 364 | ### Unhandled Requests 365 | Your server will call a `unhandledRequest` method with the HTTP `verb`, `path`, and original `request`, 366 | object if your server receives a request for a route that doesn't have a handler. By default, this method 367 | will throw an error. You can override this method to supply your own behavior: 368 | 369 | ```javascript 370 | const server = new Pretender(function() { 371 | // no routes 372 | }); 373 | 374 | server.unhandledRequest = function(verb, path, request) { 375 | console.log("what is this I don't even..."); 376 | } 377 | 378 | $.getJSON("/these/arent/the/droids"); 379 | ``` 380 | 381 | ### Pass-through Requests 382 | Requests set to be handled by pass-through will trigger the `passthroughRequest` hook: 383 | 384 | ```javascript 385 | const server = new Pretender(function() { 386 | this.get('/some/path', this.passthrough); 387 | }); 388 | 389 | server.passthroughRequest = function(verb, path, request) { 390 | console.log('request ' + path + ' successfully sent for passthrough'); 391 | } 392 | ``` 393 | 394 | 395 | ### Error Requests 396 | Your server will call a `erroredRequest` method with the HTTP `verb`, `path`, original `request`, 397 | and the original `error` object if your handler code causes an error. 398 | 399 | By default, this will augment the error message with some information about which handler caused 400 | the error and then throw the error again. You can override this method to supply your own behavior: 401 | 402 | ```javascript 403 | const server = new Pretender(function() { 404 | this.get('/api/songs', request => { 405 | undefinedWAT("this is no function!"); 406 | }); 407 | }); 408 | 409 | server.erroredRequest = function(verb, path, request, error) { 410 | SomeTestFramework.failTest(); 411 | console.warn("There was an error", error); 412 | } 413 | ``` 414 | 415 | ### Mutating the body 416 | Pretender is response format neutral, so you normally need to supply a string body as the 417 | third part of a response: 418 | 419 | ```javascript 420 | this.get('/api/songs', request => { 421 | return [200, {}, "{'id': 12}"]; 422 | }); 423 | ``` 424 | 425 | This can become tiresome if you know, for example, that all your responses are 426 | going to be JSON. The body of a response will be passed through a 427 | `prepareBody` hook before being passed to the fake response object. 428 | `prepareBody` defaults to an empty function, but can be overridden: 429 | 430 | ```javascript 431 | const server = new Pretender(function() { 432 | this.get('/api/songs', request => { 433 | return [200, {}, {id: 12}]; 434 | }); 435 | }); 436 | 437 | server.prepareBody = function(body){ 438 | return body ? JSON.stringify(body) : '{"error": "not found"}'; 439 | } 440 | ``` 441 | 442 | ### Mutating the headers 443 | Response headers can be mutated for the entire service instance by implementing a 444 | `prepareHeaders` method: 445 | 446 | ```javascript 447 | const server = new Pretender(function() { 448 | this.get('/api/songs', request => { 449 | return [200, {}, '{"id": 12}']; 450 | }); 451 | }); 452 | 453 | server.prepareHeaders = function(headers){ 454 | headers['content-type'] = 'application/javascript'; 455 | return headers; 456 | }; 457 | ``` 458 | 459 | ## Tracking Requests 460 | Your pretender instance will track handlers and requests on a few array properties. 461 | All handlers are stored on `handlers` property and incoming requests will be tracked in one of 462 | three properties: `handledRequests`, `unhandledRequests` and `passthroughRequests`. The handler is also returned from 463 | any verb function. This is useful if you want to build testing infrastructure on top of 464 | pretender and need to fail tests that have handlers without requests. 465 | You can disable tracking requests by passing `trackRequests: false` to pretender options. 466 | ```javascript 467 | const server = new Pretender({ trackRequests: false }); 468 | ``` 469 | 470 | Each handler keeps a count of the number of requests is successfully served. 471 | 472 | ```javascript 473 | server.get(/* ... */); 474 | const handler = server.handlers[0]; 475 | 476 | // or 477 | 478 | const handler = server.get(/* ... */); 479 | 480 | // then 481 | 482 | const numberOfCalls = handler.numberOfCalls; 483 | ``` 484 | 485 | ## Clean up 486 | When you're done mocking, be sure to call `shutdown()` to restore the native XMLHttpRequest object: 487 | 488 | ```javascript 489 | const server = new Pretender(function() { 490 | ... routing ... 491 | }); 492 | 493 | server.shutdown(); // all done. 494 | ``` 495 | 496 | # Development of Pretender 497 | 498 | ## Running tests 499 | 500 | * `yarn build` builds pretender 501 | * `yarn test` runs tests once 502 | * `yarn test:server` runs and reruns on changes 503 | 504 | ## Code of Conduct 505 | 506 | In order to have a more open and welcoming community this project adheres to a [code of conduct](CONDUCT.md) adapted from the [contributor covenant](http://contributor-covenant.org/). 507 | 508 | Please adhere to this code of conduct in any interactions you have with this 509 | project's community. If you encounter someone violating these terms, please let 510 | a maintainer (@trek) know and we will address it as soon as possible. 511 | -------------------------------------------------------------------------------- /test/calling_test.js: -------------------------------------------------------------------------------- 1 | var describe = QUnit.module; 2 | var it = QUnit.test; 3 | var clock; 4 | 5 | describe('pretender invoking', function(config) { 6 | config.beforeEach(function() { 7 | this.pretender = new Pretender(); 8 | }); 9 | 10 | config.afterEach(function() { 11 | if (clock) { clock.restore(); } 12 | this.pretender.shutdown(); 13 | }); 14 | 15 | it('a mapping function is optional', function(assert) { 16 | var wasCalled; 17 | 18 | this.pretender.get('/some/path', function() { 19 | wasCalled = true; 20 | }); 21 | 22 | $.ajax({ url: '/some/path' }); 23 | assert.ok(wasCalled); 24 | }); 25 | 26 | it('mapping can be called directly', function(assert) { 27 | var wasCalled; 28 | function map() { 29 | this.get('/some/path', function() { 30 | wasCalled = true; 31 | }); 32 | } 33 | 34 | this.pretender.map(map); 35 | 36 | $.ajax({ url: '/some/path' }); 37 | assert.ok(wasCalled); 38 | }); 39 | 40 | it('clobbering duplicate mapping works', function(assert) { 41 | var wasCalled, wasCalled2; 42 | function map() { 43 | this.get('/some/path', function() { 44 | wasCalled = true; 45 | }); 46 | } 47 | function map2() { 48 | this.get('/some/path', function() { 49 | wasCalled2 = true; 50 | }); 51 | } 52 | 53 | this.pretender.map(map); 54 | this.pretender.map(map2); 55 | 56 | $.ajax({ url: '/some/path' }); 57 | 58 | assert.ok(!wasCalled); 59 | assert.ok(wasCalled2); 60 | }); 61 | 62 | it('ordered duplicate mapping works', function(assert) { 63 | var wasCalled, wasCalled2; 64 | function map() { 65 | this.get('/some/path', function() { 66 | wasCalled = true; 67 | }); 68 | } 69 | 70 | this.pretender.map(map); 71 | $.ajax({ url: '/some/path' }); 72 | 73 | function map2() { 74 | this.get('/some/path', function() { 75 | wasCalled2 = true; 76 | }); 77 | } 78 | 79 | this.pretender.map(map2); 80 | $.ajax({ url: '/some/path' }); 81 | 82 | assert.ok(wasCalled); 83 | assert.ok(wasCalled2); 84 | }); 85 | 86 | it('params are passed', function(assert) { 87 | var params; 88 | this.pretender.get('/some/path/:id', function(request) { 89 | params = request.params; 90 | }); 91 | 92 | $.ajax({ url: '/some/path/1' }); 93 | assert.equal(params.id, 1); 94 | }); 95 | 96 | it('queryParams are passed', function(assert) { 97 | var params; 98 | this.pretender.get('/some/path', function(request) { 99 | params = request.queryParams; 100 | }); 101 | 102 | $.ajax({ url: '/some/path?zulu=nation' }); 103 | assert.equal(params.zulu, 'nation'); 104 | }); 105 | 106 | it('request body is accessible', function(assert) { 107 | var params; 108 | this.pretender.post('/some/path/1', function(request) { 109 | params = request.requestBody; 110 | }); 111 | 112 | $.ajax({ 113 | method: 'post', 114 | url: '/some/path/1', 115 | data: { 116 | ok: true, 117 | }, 118 | }); 119 | assert.equal(params, 'ok=true'); 120 | }); 121 | 122 | it('request headers are accessible', function(assert) { 123 | var headers; 124 | this.pretender.post('/some/path/1', function(request) { 125 | headers = request.requestHeaders; 126 | }); 127 | 128 | $.ajax({ 129 | method: 'post', 130 | url: '/some/path/1', 131 | headers: { 132 | 'A-Header': 'value', 133 | }, 134 | }); 135 | assert.equal(headers['A-Header'], 'value'); 136 | }); 137 | 138 | it('adds requests to the list of handled requests', function(assert) { 139 | this.pretender.get('/some/path', function(/*request*/) { 140 | }); 141 | 142 | $.ajax({ url: '/some/path' }); 143 | 144 | var req = this.pretender.handledRequests[0]; 145 | assert.equal(req.url, '/some/path'); 146 | }); 147 | 148 | it('increments the handler\'s request count', function(assert) { 149 | var handler = function(/*req*/) {}; 150 | 151 | this.pretender.get('/some/path', handler); 152 | 153 | $.ajax({ url: '/some/path' }); 154 | 155 | assert.equal(handler.numberOfCalls, 1); 156 | }); 157 | 158 | it('handledRequest is called', function(assert) { 159 | var done = assert.async(); 160 | 161 | var json = '{foo: "bar"}'; 162 | this.pretender.get('/some/path', function(/*req*/) { 163 | return [200, {}, json]; 164 | }); 165 | 166 | this.pretender.handledRequest = function(verb, path, request) { 167 | assert.ok(true, 'handledRequest hook was called'); 168 | assert.equal(verb, 'GET'); 169 | assert.equal(path, '/some/path'); 170 | assert.equal(request.responseText, json); 171 | assert.equal(request.response, json); 172 | assert.equal(request.status, '200'); 173 | done(); 174 | }; 175 | 176 | $.ajax({ url: '/some/path' }); 177 | }); 178 | 179 | it('prepareBody is called', function(assert) { 180 | var done = assert.async(); 181 | 182 | var obj = { foo: 'bar' }; 183 | this.pretender.prepareBody = JSON.stringify; 184 | this.pretender.get('/some/path', function(/*req*/) { 185 | return [200, {}, obj]; 186 | }); 187 | 188 | $.ajax({ 189 | url: '/some/path', 190 | success: function(resp) { 191 | assert.deepEqual(JSON.parse(resp), obj); 192 | done(); 193 | }, 194 | }); 195 | }); 196 | 197 | it('prepareHeaders is called', function(assert) { 198 | var done = assert.async(); 199 | 200 | this.pretender.prepareHeaders = function(headers) { 201 | headers['X-WAS-CALLED'] = 'YES'; 202 | return headers; 203 | }; 204 | 205 | this.pretender.get('/some/path', function(/*req*/) { 206 | return [200, {}, '']; 207 | }); 208 | 209 | $.ajax({ 210 | url: '/some/path', 211 | complete: function(xhr) { 212 | assert.equal(xhr.getResponseHeader('X-WAS-CALLED'), 'YES'); 213 | done(); 214 | }, 215 | }); 216 | }); 217 | 218 | it('will use the latest defined handler', function(assert) { 219 | assert.expect(1); 220 | 221 | var latestHandlerWasCalled = false; 222 | this.pretender.get('/some/path', function(/*request*/) { 223 | assert.ok(false); 224 | }); 225 | this.pretender.get('/some/path', function(/*request*/) { 226 | latestHandlerWasCalled = true; 227 | }); 228 | $.ajax({ url: '/some/path' }); 229 | assert.ok(latestHandlerWasCalled, 'calls the latest handler'); 230 | }); 231 | 232 | it('will error when using fully qualified URLs instead of paths', function( 233 | assert 234 | ) { 235 | var pretender = this.pretender; 236 | 237 | pretender.get('/some/path', function(/*request*/) { 238 | return [200, {}, '']; 239 | }); 240 | 241 | assert.throws(function() { 242 | pretender.handleRequest({ url: 'http://myserver.com/some/path' }); 243 | }); 244 | }); 245 | 246 | it('is resolved asynchronously', function(assert) { 247 | var done = assert.async(); 248 | var val = 'unset'; 249 | 250 | this.pretender.get('/some/path', function(/*request*/) { 251 | return [200, {}, '']; 252 | }); 253 | 254 | $.ajax({ 255 | url: '/some/path', 256 | complete: function() { 257 | assert.equal(val, 'set'); 258 | done(); 259 | }, 260 | }); 261 | 262 | assert.equal(val, 'unset'); 263 | val = 'set'; 264 | }); 265 | 266 | it('can be resolved synchronous', function(assert) { 267 | var val = 0; 268 | 269 | this.pretender.get( 270 | '/some/path', 271 | function(/*request*/) { 272 | return [200, {}, '']; 273 | }, 274 | false 275 | ); 276 | 277 | $.ajax({ 278 | url: '/some/path', 279 | complete: function() { 280 | assert.equal(val, 0); 281 | val++; 282 | }, 283 | }); 284 | 285 | assert.equal(val, 1); 286 | }); 287 | 288 | it('can be both asynchronous or synchronous based on an async function', function( 289 | assert 290 | ) { 291 | var done = assert.async(); 292 | 293 | var isAsync = false; 294 | var val = 0; 295 | 296 | this.pretender.get( 297 | '/some/path', 298 | function(/*request*/) { 299 | return [200, {}, '']; 300 | }, 301 | function() { 302 | return isAsync; 303 | } 304 | ); 305 | 306 | $.ajax({ 307 | url: '/some/path', 308 | complete: function() { 309 | assert.equal(val, 0); 310 | val++; 311 | }, 312 | }); 313 | 314 | assert.equal(val, 1); 315 | val++; 316 | 317 | isAsync = 0; 318 | 319 | $.ajax({ 320 | url: '/some/path', 321 | complete: function() { 322 | assert.equal(val, 3); 323 | done(); 324 | }, 325 | }); 326 | 327 | assert.equal(val, 2); 328 | val++; 329 | }); 330 | 331 | it('can be configured to resolve after a specified time', function(assert) { 332 | var done = assert.async(); 333 | 334 | var val = 0; 335 | 336 | this.pretender.get( 337 | '/some/path', 338 | function(/*request*/) { 339 | return [200, {}, '']; 340 | }, 341 | 100 342 | ); 343 | 344 | $.ajax({ 345 | url: '/some/path', 346 | complete: function() { 347 | assert.equal(val, 1); 348 | done(); 349 | }, 350 | }); 351 | 352 | setTimeout(function() { 353 | assert.equal(val, 0); 354 | val++; 355 | }, 0); 356 | }); 357 | 358 | it('can be configured to require manually resolution', function(assert) { 359 | var val = 0; 360 | var req = $.ajaxSettings.xhr(); 361 | 362 | this.pretender.get( 363 | '/some/path', 364 | function(/*request*/) { 365 | return [200, {}, '']; 366 | }, 367 | true 368 | ); 369 | 370 | $.ajax({ 371 | url: '/some/path', 372 | xhr: function() { 373 | // use the xhr we already made and have a reference to 374 | return req; 375 | }, 376 | complete: function() { 377 | assert.equal(val, 1); 378 | val++; 379 | }, 380 | }); 381 | 382 | assert.equal(val, 0); 383 | val++; 384 | 385 | this.pretender.resolve(req); 386 | 387 | assert.equal(val, 2); 388 | }); 389 | 390 | it('requiresManualResolution returns true for endpoints configured with `true` for async', function( 391 | assert 392 | ) { 393 | this.pretender.get('/some/path', function(/*request*/) {}, true); 394 | this.pretender.get('/some/other/path', function() {}); 395 | 396 | assert.ok(this.pretender.requiresManualResolution('get', '/some/path')); 397 | assert.ok( 398 | !this.pretender.requiresManualResolution('get', '/some/other/path') 399 | ); 400 | }); 401 | 402 | it( 403 | 'async requests with `onprogress` upload events in the upload ' + 404 | ' trigger a progress event each 50ms', 405 | function(assert) { 406 | clock = sinon.useFakeTimers(); 407 | var progressEventCount = 0; 408 | this.pretender.post( 409 | '/uploads', 410 | function(/*request*/) { 411 | return [200, {}, '']; 412 | }, 413 | 300 414 | ); 415 | 416 | var xhr = new window.XMLHttpRequest(); 417 | xhr.open('POST', '/uploads'); 418 | var lastLoaded = 0; 419 | var postBody = 'some data'; 420 | xhr.upload.onprogress = function(event) { 421 | var loaded = event.loaded; 422 | var total = event.total; 423 | assert.equal(total, postBody.length, 'ProgressEvent has total of requestBody byte size'); 424 | assert.ok(loaded > lastLoaded, 'making progress'); 425 | assert.ok(loaded <= total, 'loaded should always not exceed total'); 426 | progressEventCount++; 427 | }; 428 | xhr.send(postBody); 429 | clock.tick(310); 430 | assert.equal( 431 | progressEventCount, 432 | 6, 433 | 'In a request of 300ms the progress event has been fired 5 times' 434 | ); 435 | } 436 | ); 437 | 438 | it('`onprogress` returns correct values for loaded, total in case of FormData requestbody', function(assert) { 439 | clock = sinon.useFakeTimers(); 440 | this.pretender.post('/uploads', function(/*request*/) { 441 | return [200, {}, '']; 442 | }, 210); 443 | 444 | var progressEventCount = 0; 445 | var xhr = new window.XMLHttpRequest(); 446 | xhr.open('POST', '/uploads'); 447 | var formData = new FormData(); 448 | formData.set('field1', 'some value'); 449 | formData.set('field2', 'another value'); 450 | formData.set('file1', new Blob(['some file'])); 451 | formData.set('file2', new Blob(['another file'])); 452 | var totalSize = 10 + 13 + 9 + 12; // sum of the above fields 453 | var lastLoaded = 0; 454 | xhr.upload.onprogress = function(event) { 455 | var loaded = event.loaded; 456 | var total = event.total; 457 | assert.equal(total, totalSize, 'ProgressEvent has total of requestBody byte size'); 458 | assert.ok(loaded > lastLoaded, 'making progress'); 459 | assert.ok(loaded <= total, 'loaded should always not exceed total'); 460 | lastLoaded = loaded; 461 | progressEventCount++; 462 | }; 463 | xhr.send(formData); 464 | clock.tick(510); 465 | assert.equal( 466 | progressEventCount, 467 | 5, 468 | 'No `onprogress` events are fired after the the request finalizes' 469 | ); 470 | assert.equal(lastLoaded, totalSize, 'Final progress event should match total body size'); 471 | }); 472 | 473 | it('`onprogress` upload events don\'t keep firing once the request has ended', function( 474 | assert 475 | ) { 476 | clock = sinon.useFakeTimers(); 477 | var progressEventCount = 0; 478 | this.pretender.post( 479 | '/uploads', 480 | function(/*request*/) { 481 | return [200, {}, '']; 482 | }, 483 | 210 484 | ); 485 | 486 | var xhr = new window.XMLHttpRequest(); 487 | xhr.open('POST', '/uploads'); 488 | var lastLoaded = 0; 489 | var postBody = new Blob(['some data']); 490 | xhr.upload.onprogress = function(event) { 491 | var loaded = event.loaded; 492 | var total = event.total; 493 | assert.equal(total, postBody.size, 'ProgressEvent has total of requestBody byte size'); 494 | assert.ok(loaded > lastLoaded, 'making progress'); 495 | assert.ok(loaded <= total, 'loaded should always not exceed total'); 496 | progressEventCount++; 497 | lastLoaded = loaded; 498 | }; 499 | xhr.send(postBody); 500 | clock.tick(510); 501 | assert.equal( 502 | progressEventCount, 503 | 5, 504 | 'No `onprogress` events are fired after the the request finalizes' 505 | ); 506 | assert.equal(lastLoaded, postBody.size, 'Final progress event should match total body size'); 507 | }); 508 | 509 | it('no progress upload events are fired after the request is aborted', function( 510 | assert 511 | ) { 512 | var progressEventCount = 0; 513 | 514 | clock = sinon.useFakeTimers(); 515 | 516 | this.pretender.post( 517 | '/uploads', 518 | function(/*request*/) { 519 | return [200, {}, '']; 520 | }, 521 | 210 522 | ); 523 | 524 | var xhr = new window.XMLHttpRequest(); 525 | xhr.open('POST', '/uploads'); 526 | var postBody = new ArrayBuffer(8); 527 | xhr.upload.onprogress = function(event) { 528 | var loaded = event.loaded; 529 | var total = event.total; 530 | assert.equal(total, postBody.byteLength, 'ProgressEvent has total of requestBody byte size'); 531 | assert.ok(loaded > 0, 'making progress'); 532 | assert.ok(loaded <= total, 'loaded should always not exceed total'); 533 | progressEventCount++; 534 | }; 535 | xhr.send(postBody); 536 | 537 | clock.tick(90); 538 | xhr.abort(); 539 | clock.tick(220); 540 | assert.equal( 541 | progressEventCount, 542 | 1, 543 | 'only one progress event was triggered because the request was aborted' 544 | ); 545 | }); 546 | 547 | it('async requests with `onprogress` events trigger a progress event each 50ms', function( 548 | assert 549 | ) { 550 | clock = sinon.useFakeTimers(); 551 | var progressEventCount = 0; 552 | this.pretender.get( 553 | '/downloads', 554 | function(/*request*/) { 555 | return [200, {}, '']; 556 | }, 557 | 300 558 | ); 559 | 560 | var xhr = new window.XMLHttpRequest(); 561 | xhr.open('GET', '/downloads'); 562 | xhr.onprogress = function(event) { 563 | assert.equal(event.total, 0, 'GET request has no requestBody'); 564 | assert.equal(event.loaded, event.total); 565 | progressEventCount++; 566 | }; 567 | xhr.send('some data'); 568 | clock.tick(310); 569 | assert.equal( 570 | progressEventCount, 571 | 6, 572 | 'In a request of 300ms the progress event has been fired 5 times' 573 | ); 574 | }); 575 | 576 | it('`onprogress` download events don\'t keep firing once the request has ended', function( 577 | assert 578 | ) { 579 | clock = sinon.useFakeTimers(); 580 | var progressEventCount = 0; 581 | this.pretender.get( 582 | '/downloads', 583 | function(/*request*/) { 584 | return [200, {}, '']; 585 | }, 586 | 210 587 | ); 588 | 589 | var xhr = new window.XMLHttpRequest(); 590 | xhr.open('GET', '/downloads'); 591 | xhr.onprogress = function(/*e*/) { 592 | progressEventCount++; 593 | }; 594 | xhr.send('some data'); 595 | clock.tick(510); 596 | assert.equal( 597 | progressEventCount, 598 | 5, 599 | 'No `onprogress` events are fired after the the request finalizes' 600 | ); 601 | }); 602 | 603 | it('no progress download events are fired after the request is aborted', function( 604 | assert 605 | ) { 606 | var progressEventCount = 0; 607 | clock = sinon.useFakeTimers(); 608 | this.pretender.get( 609 | '/downloads', 610 | function(/*request*/) { 611 | return [200, {}, '']; 612 | }, 613 | 210 614 | ); 615 | 616 | var xhr = new window.XMLHttpRequest(); 617 | xhr.open('GET', '/downloads'); 618 | xhr.onprogress = function(/*e*/) { 619 | progressEventCount++; 620 | }; 621 | xhr.send('some data'); 622 | clock.tick(90); 623 | xhr.abort(); 624 | clock.tick(220); 625 | assert.equal( 626 | progressEventCount, 627 | 1, 628 | 'only one progress event was triggered because the request was aborted' 629 | ); 630 | }); 631 | 632 | it('resolves cross-origin requests', function(assert) { 633 | var url = 'http://status.github.com/api/status'; 634 | var payload = 'it works!'; 635 | var wasCalled; 636 | 637 | this.pretender.get(url, function() { 638 | wasCalled = true; 639 | return [200, {}, payload]; 640 | }); 641 | 642 | $.ajax({ url: url }); 643 | assert.ok(wasCalled); 644 | }); 645 | 646 | it('accepts a handler that returns a promise', function(assert) { 647 | var done = assert.async(); 648 | 649 | var json = '{foo: "bar"}'; 650 | 651 | this.pretender.get('/some/path', function(/*req*/) { 652 | return new Promise(function(resolve) { 653 | resolve([200, {}, json]); 654 | }); 655 | }); 656 | 657 | this.pretender.handledRequest = function(verb, path, request) { 658 | assert.ok(true, 'handledRequest hook was called'); 659 | assert.equal(verb, 'GET'); 660 | assert.equal(path, '/some/path'); 661 | assert.equal(request.responseText, json); 662 | assert.equal(request.response, json); 663 | assert.equal(request.status, '200'); 664 | done(); 665 | }; 666 | 667 | $.ajax({ url: '/some/path' }); 668 | }); 669 | }); 670 | --------------------------------------------------------------------------------