├── .circleci └── config.yml ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── CODE_OF_CONDUCT.md ├── Gruntfile.js ├── MIT.LICENSE ├── README.markdown ├── bower.json ├── lib └── mock-ajax.js ├── package.json ├── release_notes ├── 2.0.2.md ├── 2.99.md ├── 3.0.md ├── 3.1.0.md ├── 3.1.1.md ├── 3.2.0.md ├── 3.3.0.md ├── 3.3.1.md ├── 3.4.0.md └── 4.0.0.md ├── scripts ├── ci.sh ├── run-all-browsers ├── run-in-browser.js ├── start-sauce-connect └── stop-sauce-connect ├── spec ├── eventBusSpec.js ├── eventSpec.js ├── fakeRequestSpec.js ├── helpers │ └── spec-helper.js ├── integration │ ├── mock-ajax-spec.js │ ├── webmock-style-spec.js │ └── with-mock-spec.js ├── mock-ajax-toplevel-spec.js ├── paramParserSpec.js ├── requestStubSpec.js ├── requestTrackerSpec.js ├── stubTrackerSpec.js └── support │ └── jasmine-browser.js └── src ├── boot.js ├── event.js ├── eventBus.js ├── fakeRequest.js ├── mockAjax.js ├── paramParser.js ├── requestStub.js ├── requestTracker.js ├── requireAjax.js └── stubTracker.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | executors: 4 | node: 5 | docker: 6 | - image: cimg/node:16.14.0 # Latest 16.x 7 | 8 | jobs: 9 | test_browsers: 10 | executor: node 11 | environment: 12 | USE_SAUCE: "true" 13 | steps: 14 | - checkout 15 | - run: 16 | name: Report versions 17 | command: "echo 'Node version:' && node -v && echo 'NPM version:' && npm -v" 18 | - run: 19 | name: Install NPM packages 20 | command: "npm install" 21 | - run: 22 | name: Install Sauce Connect 23 | command: | 24 | tmpdir=$(mktemp -d) 25 | cd "$tmpdir" 26 | curl https://saucelabs.com/downloads/sauce-connect/5.2.2/sauce-connect-5.2.2_linux.x86_64.tar.gz | tar zxf - 27 | chmod +x sc 28 | mkdir ~/bin 29 | cp sc ~/bin 30 | echo "Sauce Connect version info:" 31 | ~/bin/sc version 32 | - run: 33 | name: Run tests 34 | command: scripts/ci.sh 35 | 36 | workflows: 37 | version: 2 38 | push: &push_workflow 39 | jobs: 40 | - test_browsers 41 | cron: 42 | <<: *push_workflow 43 | triggers: 44 | - schedule: 45 | # Times are UTC. 46 | cron: "0 10 * * 1" 47 | filters: 48 | branches: 49 | only: 50 | - main 51 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Developing for Jasmine Ajax 2 | 3 | We welcome your contributions! Thanks for helping make Jasmine a better project for everyone. Please review the backlog and discussion lists before starting work. What you're looking for may already have been done. If it hasn't, the community can help make your contribution better. If you want to contribute but don't know what to work on, [issues tagged ready for work](https://github.com/jasmine/jasmine-ajax/labels/ready%20for%20work) should have enough detail to get started. 4 | 5 | ## Links 6 | 7 | - [Jasmine Google Group](http://groups.google.com/group/jasmine-js) 8 | - [Jasmine-dev Google Group](http://groups.google.com/group/jasmine-js-dev) 9 | - [Jasmine on PivotalTracker](https://www.pivotaltracker.com/n/projects/10606) 10 | 11 | ## General Workflow 12 | 13 | Please submit pull requests via feature branches using the semi-standard workflow of: 14 | 15 | ```bash 16 | git clone git@github.com:yourUserName/jasmine-ajax.git # Clone your fork 17 | cd jasmine-ajax # Change directory 18 | git remote add upstream https://github.com/jasmine/jasmine-ajax.git # Assign original repository to a remote named 'upstream' 19 | git fetch upstream # Fetch changes not present in your local repository 20 | git merge upstream/main # Sync local main with upstream repository 21 | git checkout -b my-new-feature # Create your feature branch 22 | git commit -am 'Add some feature' # Commit your changes 23 | git push origin my-new-feature # Push to the branch 24 | ``` 25 | 26 | Once you've pushed a feature branch to your forked repo, you're ready to open a pull request. We favor pull requests with very small, single commits with a single purpose. 27 | 28 | ## Background 29 | 30 | ### Directory Structure 31 | 32 | * `/src` contains all of the source files 33 | * `/spec` contains all of the tests 34 | * mirrors the source directory 35 | * there are some additional files 36 | * `/lib` contains the generated files for distribution 37 | 38 | ### Install Dependencies 39 | 40 | Jasmine Ajax relies on Node.js. 41 | 42 | To install the Node dependencies, you will need Node.js, Npm, and [Grunt](http://gruntjs.com/), the [grunt-cli](https://github.com/gruntjs/grunt-cli) and ensure that `grunt` is on your path. 43 | 44 | $ npm install --local 45 | 46 | ...will install all of the node modules locally. Now run 47 | 48 | $ grunt 49 | 50 | ...if you see that JSHint runs, your system is ready. 51 | 52 | ### How to write new Jasmine Ajax code 53 | 54 | Or, How to make a successful pull request 55 | 56 | * _Do not change the public interface_. Lots of projects depend on Jasmine and if you aren't careful you'll break them 57 | * _Be browser agnostic_ - if you must rely on browser-specific functionality, please write it in a way that degrades gracefully 58 | * _Write specs_ - Jasmine's a testing framework; don't add functionality without test-driving it 59 | * _Write code in the style of the rest of the repo_ - Jasmine should look like a cohesive whole 60 | * _Ensure the *entire* test suite is green_ in all the big browsers and JSHint - your contribution shouldn't break Jasmine for other users 61 | 62 | Follow these tips and your pull request, patch, or suggestion is much more likely to be integrated. 63 | 64 | ### Running Specs 65 | 66 | Jasmine Ajax uses [jasmine-browser-runner](http://github.com/jasmine/jasmine-browser-runner) to test itself in browser. 67 | 68 | $ npx jasmine-browser-runner 69 | 70 | ...and then visit `http://localhost:8888` to run specs. 71 | 72 | ## Before Committing or Submitting a Pull Request 73 | 74 | 1. Ensure all specs are green in browser 75 | 1. Ensure JSHint is green with `grunt jshint` 76 | 1. Build `mock-ajax.js` with `grunt build` 77 | 1. Make sure the tests pass 78 | 79 | Note that we use Circle for Continuous Integration. We only accept green pull requests. 80 | 81 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Are you creating an issue in the correct repository? 2 | 3 | - This repository is for the AJAX mocking functionality 4 | - If you have an issue with the Jasmine docs, file an issue in the docs repo 5 | here: https://github.com/jasmine/jasmine.github.io 6 | 7 | 8 | 9 | ## Expected Behavior 10 | 11 | 12 | 13 | ## Current Behavior 14 | 15 | 16 | 17 | ## Possible Solution 18 | 19 | 20 | 21 | ## Suite that reproduces the behavior (for bugs) 22 | 23 | ```javascript 24 | describe("sample", function() { 25 | }); 26 | ``` 27 | ## Context 28 | 29 | 30 | 31 | ## Your Environment 32 | 33 | * Version used: 34 | * Environment name and version (e.g. Chrome 39, node.js 5.4): 35 | * Operating System and version (desktop or mobile): 36 | * Link to your project: 37 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Motivation and Context 7 | 8 | 9 | 10 | ## How Has This Been Tested? 11 | 12 | 13 | 14 | 15 | ## Types of changes 16 | 17 | - [ ] Bug fix (non-breaking change which fixes an issue) 18 | - [ ] New feature (non-breaking change which adds functionality) 19 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 20 | 21 | ## Checklist: 22 | 23 | 24 | - [ ] My code follows the code style of this project. 25 | - [ ] My change requires a change to the documentation. 26 | - [ ] I have updated the documentation accordingly. 27 | - [ ] I have read the **CONTRIBUTING** document. 28 | - [ ] I have generated the `mock-ajax.js` file with `grunt build` 29 | - [ ] I have added tests to cover my changes. 30 | - [ ] All new and existing tests passed. 31 | 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .tmp/ 3 | .rvmrc 4 | *.swp 5 | bower_components 6 | node_modules 7 | package-lock.json 8 | .ruby-version 9 | Gemfile.lock 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | **/.* 2 | Gemfile 3 | Gemfile.lock 4 | Gruntfile.js 5 | Rakefile 6 | bower_components 7 | node_modules 8 | spec 9 | src 10 | travis-script.sh 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at jasmine-maintainers@googlegroups.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function( grunt ) { 2 | 'use strict'; 3 | 4 | function packageVersion() { 5 | return require('./package.json').version; 6 | } 7 | // 8 | // Grunt configuration: 9 | // 10 | // https://github.com/cowboy/grunt/blob/master/docs/getting_started.md 11 | // 12 | grunt.initConfig({ 13 | // specifying JSHint options and globals 14 | // https://github.com/cowboy/grunt/blob/master/docs/task_lint.md#specifying-jshint-options-and-globals 15 | jshint: { 16 | options: { 17 | boss: true, 18 | browser: true, 19 | curly: true, 20 | eqeqeq: true, 21 | eqnull: true, 22 | immed: true, 23 | latedef: true, 24 | newcap: true, 25 | noarg: true, 26 | sub: true, 27 | undef: true, 28 | globals: { 29 | global: true, 30 | jasmine: false, 31 | module: false, 32 | exports: true, 33 | describe: false, 34 | it: false, 35 | expect: false, 36 | beforeEach: false, 37 | afterEach: false, 38 | spyOn: false, 39 | getJasmineRequireObj: false, 40 | require: false 41 | } 42 | }, 43 | all: ['Gruntfile.js', 'src/**/*.js', 'lib/**/*.js', 'spec/**/*.js'] 44 | }, 45 | packageVersion: packageVersion(), 46 | shell: { 47 | ctags: { 48 | command: 'ctags -R lib' 49 | }, 50 | release: { 51 | command: [ 52 | 'git tag v<%= packageVersion %>', 53 | 'git push origin main --tags', 54 | 'npm publish' 55 | ].join('&&') 56 | } 57 | }, 58 | template: { 59 | options: { 60 | data: function() { 61 | return { 62 | packageVersion: packageVersion(), 63 | files: grunt.file.expand([ 64 | 'src/requireAjax.js', 65 | 'src/**/*.js', 66 | '!src/boot.js' 67 | ]) 68 | }; 69 | } 70 | }, 71 | lib: { 72 | src: 'src/boot.js', 73 | dest: '.tmp/mock-ajax.js' 74 | } 75 | }, 76 | includes: { 77 | options: { 78 | includeRegexp: /\/\/\s*include "(\S+)";/, 79 | includePath: '.' 80 | }, 81 | lib: { 82 | src: '.tmp/mock-ajax.js', 83 | dest: 'lib/mock-ajax.js' 84 | } 85 | } 86 | }); 87 | 88 | grunt.registerTask('versionCheck', function() { 89 | var pkgVersion = packageVersion(), 90 | bower = require('./bower.json'), 91 | bowerVersion = bower.version; 92 | 93 | if (pkgVersion !== bowerVersion) { 94 | grunt.fail.fatal("package.json and bower.json have different version numbers\n\tpackage.json:\t" + pkgVersion + "\n\tbower.json:\t" + bowerVersion); 95 | } 96 | }); 97 | 98 | grunt.loadNpmTasks('grunt-contrib-jshint'); 99 | grunt.loadNpmTasks('grunt-template'); 100 | grunt.loadNpmTasks('grunt-includes'); 101 | grunt.loadNpmTasks('grunt-shell'); 102 | 103 | grunt.registerTask('default', ['jshint']); 104 | grunt.registerTask('build', ['template:lib', 'includes:lib']); 105 | grunt.registerTask('ctags', 'Generate ctags', ['shell:ctags']); 106 | grunt.registerTask('release', 'Release ' + packageVersion() + ' to npm', ['versionCheck', 'shell:release']); 107 | }; 108 | -------------------------------------------------------------------------------- /MIT.LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2019 Pivotal Labs 2 | Copyright (c) 2012-2024 The Jasmine developers 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | jasmine-ajax - Faking Ajax responses in your Jasmine suite. 2 | ============ 3 | jasmine-ajax is a library that lets you define a set of fake responses for Ajax 4 | requests made by your application, specify per spec which response should be 5 | used, and keep track of the Ajax requests you make so you can make assertions 6 | about the results. 7 | 8 | Libraries Supported 9 | ------------------- 10 | jasmine-ajax is currently compatible with any library that uses XMLHttpRequest. 11 | 12 | Installing 13 | ---------- 14 | #### NPM #### 15 | Install `jasmine-ajax` from NPM via `npm install --save-dev jasmine-ajax`; you can then `require('jasmine-ajax')` inside your test-suite and access it via the `jasmine` global. 16 | 17 | #### Browser #### 18 | Download [mock-ajax.js](https://raw.github.com/jasmine/jasmine-ajax/main/lib/mock-ajax.js) and add it to your project. If you are using 19 | jasmine-browser-runner, be sure the location you put mock-ajax.js is included 20 | in your `helpers` path in jasmine-browser.json. If you are using Jasmine 21 | standalone, make sure you add it to your spec runner. 22 | 23 | Setup 24 | ----- 25 | Using the library in your Jasmine specs consists of five parts: 26 | 27 | 1. Defining test responses 28 | 2. Installing the mock 29 | 3. Triggering the ajax request code 30 | 3. Defining the response for each request 31 | 4. Inspecting Ajax requests and setting expectations on them 32 | 33 | Example 34 | ------- 35 | Let's use a simple Foursquare venue search app to show each of these steps. 36 | 37 | ### 1. Defining Test Responses ### 38 | After signing up for an API key and playing around with curl a bit you should have an idea of what API resources you are interested in and what sample responses look like. Once you do, you can define simple JavaScripts objects that will be used to build XMLHttpRequest objects later. 39 | 40 | For example, if you have a response that looks like this: 41 | ```json 42 | { 43 | "meta":{ 44 | "code":200, 45 | "errorType":"deprecated", 46 | "errorDetail":"This endpoint will stop returning groups in the future. Please use a current version, see http://bit.ly/lZx3NU." 47 | }, 48 | "response":{ 49 | "groups":[{ 50 | "type":"nearby", 51 | "name":"Nearby", 52 | "items":[{ 53 | "id":"4bb9fd9f3db7b7138dbd229a", 54 | "name":"Pivotal Labs", 55 | "contact":{ 56 | "twitter":"pivotalboulder" 57 | }, 58 | "location":{ 59 | "address":"1701 Pearl St.", 60 | "crossStreet":"at 17th St.", 61 | "city":"Boulder", 62 | "state":"CO", 63 | "lat":40.019461, 64 | "lng":-105.273296, 65 | "distance":0 66 | }, 67 | "categories":[{ 68 | "id":"4bf58dd8d48988d124941735", 69 | "name":"Office", 70 | "pluralName":"Offices", 71 | "icon":"https://foursquare.com/img/categories/building/default.png", 72 | "parents":["Homes, Work, Others" 73 | ], 74 | "primary":true 75 | } 76 | ], 77 | "verified":false, 78 | "stats":{ 79 | "checkinsCount":223, 80 | "usersCount":62 81 | }, 82 | "hereNow":{ 83 | "count":0 84 | } 85 | } 86 | ] 87 | } 88 | ] 89 | } 90 | } 91 | ``` 92 | 93 | Then you'd define a mock response that looks something like this: 94 | 95 | ```javascript 96 | var TestResponses = { 97 | search: { 98 | success: { 99 | status: 200, 100 | responseText: '{"response":{"groups":[{"type":"nearby","name":"Nearby","items":[{"id":"4bb9fd9f3db7b7138dbd229a","name":"Pivotal Labs","contact":{"twitter":"pivotalboulder"},"location":{"address":"1701 Pearl St.","crossStreet":"at 17th St.","city":"Boulder","state":"CO","lat":40.019461,"lng":-105.273296,"distance":0},"categories":[{"id":"4bf58dd8d48988d124941735","name":"Office","pluralName":"Offices","icon":"https://foursquare.com/img/categories/building/default.png","parents":["Homes, Work, Others"],"primary":true}],"verified":false,"stats":{"checkinsCount":223,"usersCount":62},"hereNow":{"count":0}},{"id":"4af2eccbf964a5203ae921e3","name":"Laughing Goat Café","contact":{},"location":{"address":"1709 Pearl St.","crossStreet":"btw 16th & 17th","city":"Boulder","state":"CO","postalCode":"80302","country":"USA","lat":40.019321,"lng":-105.27311982,"distance":21},"categories":[{"id":"4bf58dd8d48988d1e0931735","name":"Coffee Shop","pluralName":"Coffee Shops","icon":"https://foursquare.com/img/categories/food/coffeeshop.png","parents":["Food"],"primary":true},{"id":"4bf58dd8d48988d1a7941735","name":"College Library","pluralName":"College Libraries","icon":"https://foursquare.com/img/categories/education/default.png","parents":["Colleges & Universities"]}],"verified":false,"stats":{"checkinsCount":1314,"usersCount":517},"hereNow":{"count":0}},{"id":"4ca777a597c8a1cdf7bc7aa5","name":"Ted\'s Montana Grill","contact":{"phone":"3034495546","formattedPhone":"(303) 449-5546","twitter":"TedMontanaGrill"},"location":{"address":"1701 Pearl St.","crossStreet":"17th and Pearl","city":"Boulder","state":"CO","postalCode":"80302","country":"USA","lat":40.019376,"lng":-105.273311,"distance":9},"categories":[{"id":"4bf58dd8d48988d1cc941735","name":"Steakhouse","pluralName":"Steakhouses","icon":"https://foursquare.com/img/categories/food/steakhouse.png","parents":["Food"],"primary":true}],"verified":true,"stats":{"checkinsCount":197,"usersCount":150},"url":"http://www.tedsmontanagrill.com/","hereNow":{"count":0}},{"id":"4d3cac5a8edf3704e894b2a5","name":"Pizzeria Locale","contact":{},"location":{"address":"1730 Pearl St","city":"Boulder","state":"CO","postalCode":"80302","country":"USA","lat":40.0193746,"lng":-105.2726744,"distance":53},"categories":[{"id":"4bf58dd8d48988d1ca941735","name":"Pizza Place","pluralName":"Pizza Places","icon":"https://foursquare.com/img/categories/food/pizza.png","parents":["Food"],"primary":true}],"verified":false,"stats":{"checkinsCount":511,"usersCount":338},"hereNow":{"count":2}},{"id":"4d012cd17c56370462a6b4f0","name":"The Pinyon","contact":{},"location":{"address":"1710 Pearl St.","city":"Boulder","state":"CO","country":"USA","lat":40.019219,"lng":-105.2730563,"distance":33},"categories":[{"id":"4bf58dd8d48988d14e941735","name":"American Restaurant","pluralName":"American Restaurants","icon":"https://foursquare.com/img/categories/food/default.png","parents":["Food"],"primary":true}],"verified":true,"stats":{"checkinsCount":163,"usersCount":98},"hereNow":{"count":1}}]}]}}' 101 | } 102 | } 103 | }; 104 | ``` 105 | 106 | A good place to define this is in `spec/javascripts/helpers/test_responses`. You can also define failure responses, for whatever status codes the API you are working with supports. 107 | 108 | ### 2. Installing the mock ### 109 | Install the mock using `jasmine.Ajax.install()`: 110 | 111 | ```javascript 112 | beforeEach(function() { 113 | jasmine.Ajax.install(); 114 | ... 115 | }); 116 | 117 | // don't forget to uninstall as well... 118 | afterEach(function() { 119 | jasmine.Ajax.uninstall(); 120 | ... 121 | }); 122 | ``` 123 | After this, all Ajax requests will be captured by jasmine-ajax. If you want to do things like load fixtures, do it before you install the mock (see below). 124 | 125 | ### 3. Trigger ajax request code ### 126 | Before you can specify that a request uses your test response, you must have a handle to the request itself. This means that the request is made first by the code under test and then you will set your test response (see next step). 127 | 128 | ```javascript 129 | foursquare.search('40.019461,-105.273296', { 130 | onSuccess: onSuccess, 131 | onFailure: onFailure 132 | }); 133 | 134 | request = jasmine.Ajax.requests.mostRecent(); 135 | ``` 136 | 137 | The onreadystatechange event isn't fired to complete the ajax request until you set the response in the next step. 138 | 139 | ### 4. Set responses ### 140 | Now that you've defined some test responses and installed the mock, you need to tell jasmine-ajax which response to use for a given spec. If you want to use your success response for a set of related success specs, you might use: 141 | 142 | ```javascript 143 | describe("on success", function() { 144 | beforeEach(function() { 145 | request.respondWith(TestResponses.search.success); 146 | }); 147 | }); 148 | ``` 149 | 150 | Now for all the specs in this example group, whenever an Ajax response is sent, it will use the `TestResponses.search.success` object defined in your test responses to build the XMLHttpRequest object. 151 | 152 | ### 5. Inspect Ajax requests ### 153 | Putting it all together, you can install the mock, pass some spies as callbacks to your search object, and make expectations about the expected behavior. 154 | 155 | ```javascript 156 | describe("FoursquareVenueSearch", function() { 157 | var foursquare, request; 158 | var onSuccess, onFailure; 159 | 160 | beforeEach(function() { 161 | jasmine.Ajax.install(); 162 | 163 | onSuccess = jasmine.createSpy('onSuccess'); 164 | onFailure = jasmine.createSpy('onFailure'); 165 | 166 | foursquare = new FoursquareVenueSearch(); 167 | 168 | foursquare.search('40.019461,-105.273296', { 169 | onSuccess: onSuccess, 170 | onFailure: onFailure 171 | }); 172 | 173 | request = jasmine.Ajax.requests.mostRecent(); 174 | expect(request.url).toBe('venues/search'); 175 | expect(request.method).toBe('POST'); 176 | expect(request.data()).toEqual({latLng: ['40.019461, -105.273296']}); 177 | }); 178 | 179 | describe("on success", function() { 180 | beforeEach(function() { 181 | request.respondWith(TestResponses.search.success); 182 | }); 183 | 184 | it("calls onSuccess with an array of Locations", function() { 185 | expect(onSuccess).toHaveBeenCalled(); 186 | 187 | var successArgs = onSuccess.calls.mostRecent().args[0]; 188 | 189 | expect(successArgs.length).toEqual(1); 190 | expect(successArgs[0]).toEqual(jasmine.any(Venue)); 191 | }); 192 | }); 193 | }); 194 | ``` 195 | 196 | By default the `data` function is very naive about parsing form data being sent. 197 | 198 | The provided parsers are: 199 | 200 | 1. If the XHR has a content-type of application/json, JSON.parse 201 | 1. Otherwise simply split query string by '&' and '=' 202 | 203 | If you need more control over how your data is presented, you can supply a custom param parser. Custom parsers will be prepended to the list of parsers to try. 204 | 205 | ```javascript 206 | describe("custom params", function() { 207 | beforeEach(function() { 208 | jasmine.Ajax.install(); 209 | jasmine.Ajax.addCustomParamParser({ 210 | test: function(xhr) { 211 | // return true if you can parse 212 | }, 213 | parse: function(params) { 214 | // parse and return 215 | } 216 | }); 217 | }); 218 | }); 219 | ``` 220 | 221 | 222 | Loading Fixtures 223 | ---------------- 224 | Most third-party Jasmine extensions use Ajax to load HTML fixtures into the DOM. Since jasmine-ajax intercepts all Ajax calls after it is installed, you need to load your fixtures before installing the mock. If you are using jasmine-jquery, that looks like this: 225 | 226 | ```javascript 227 | beforeEach(function(){ 228 | // first load your fixtures 229 | loadFixtures('fixture.html'); 230 | 231 | // then install the mock 232 | jasmine.Ajax.install(); 233 | }); 234 | ``` 235 | 236 | 237 | Complex Requests 238 | ---------------- 239 | Third-party frameworks may do many requests you can not simply respond to. 240 | Let's assume that you are talking to a SOAP service. SOAP services mostly have identical URL's, but responses differ by the XML request that was send using a POST request. 241 | Let's register a response that will be used if the request body was matched against a RegExp. 242 | 243 | ```javascript 244 | beforeEach(function(){ 245 | // first install the mock 246 | jasmine.Ajax.install(); 247 | 248 | // then register a request to which automatically will be responded 249 | jasmine.Ajax.stubRequest( 250 | 'https://soap.domain.tld/ws/UserManager', 251 | /.*\.*/ 252 | ).andReturn({ 253 | status: 200, 254 | statusText: 'HTTP/1.1 200 OK', 255 | contentType: 'text/xml;charset=UTF-8', 256 | responseText: 'foobar' 257 | }); 258 | 259 | // Register another response for the same URL, but with different SOAP request 260 | jasmine.Ajax.stubRequest( 261 | 'https://soap.domain.tld/ws/UserManager', 262 | /.*\.*/ 263 | ).andReturn({ 264 | status: 200, 265 | statusText: 'HTTP/1.1 200 OK', 266 | contentType: 'text/xml;charset=UTF-8', 267 | responseText: 'true' 268 | }); 269 | }); 270 | ``` 271 | 272 | Or if you also want to avoid the host part of the URL, you can register it using a RegExp for the URL, too. 273 | 274 | ```javascript 275 | beforeEach(function(){ 276 | // first install the mock 277 | jasmine.Ajax.install(); 278 | 279 | // then register a request to which automatically will be responded 280 | jasmine.Ajax.stubRequest( 281 | /.*\/ws\/UserManager/, 282 | /.*\.*/ 283 | ).andReturn({ 284 | status: 200, 285 | statusText: 'HTTP/1.1 200 OK', 286 | contentType: 'text/xml;charset=UTF-8', 287 | responseText: 'foobar' 288 | }); 289 | }); 290 | ``` 291 | 292 | [Additional documentation is available at the Jasmine docs site](https://jasmine.github.io/tutorials/mocking_ajax). 293 | 294 | Contributing 295 | ------------ 296 | Please read the main Jasmine [contributors' guide](https://github.com/jasmine/jasmine/blob/main/.github/CONTRIBUTING.md) and 297 | [specifics for Jasmine Ajax](https://github.com/jasmine/jasmine-ajax/blob/main/.github/CONTRIBUTING.md). 298 | 299 | When submitting a pull request, run `grunt build` and commit the changes to the 300 | generated file `lib/mock-ajax.js` in order for the build's automated tests to 301 | pass. 302 | 303 | 304 | Jasmine 305 | ------- 306 | http://jasmine.github.io 307 | 308 | Copyright (c) 2010-2019 Pivotal Labs.
309 | Copyright (c) 2012-2024 The Jasmine developers.
310 | This software is licensed under the MIT License. 311 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jasmine-ajax", 3 | "description": "A library for faking Ajax responses in your Jasmine suite.", 4 | "version": "4.0.0", 5 | "main": "lib/mock-ajax.js", 6 | "license": "MIT", 7 | 8 | "homepage": "https://github.com/jasmine/jasmine-ajax", 9 | "authors": [ 10 | "JR Boyens ", 11 | "Gregg Van Hove " 12 | ], 13 | "dependencies": { 14 | "jasmine" : "~3" 15 | }, 16 | "moduleType": [ 17 | "globals" 18 | ], 19 | "keywords": [ 20 | "jasmine", 21 | "ajax" 22 | ], 23 | "ignore": [ 24 | "**/.*", 25 | "Gemfile", 26 | "Gemfile.lock", 27 | "Gruntfile.js", 28 | "Rakefile", 29 | "bower_components", 30 | "node_modules", 31 | "spec", 32 | "src", 33 | "travis-script.sh" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /lib/mock-ajax.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Jasmine-Ajax - v3.4.0: a set of helpers for testing AJAX requests under the Jasmine 4 | BDD framework for JavaScript. 5 | 6 | http://github.com/jasmine/jasmine-ajax 7 | 8 | Jasmine Home page: http://jasmine.github.io/ 9 | 10 | Copyright (c) 2008-2015 Pivotal Labs 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | */ 32 | // jshint latedef: nofunc 33 | 34 | //Module wrapper to support both browser and CommonJS environment 35 | (function (root, factory) { 36 | // if (typeof exports === 'object' && typeof exports.nodeName !== 'string') { 37 | // // CommonJS 38 | // var jasmineRequire = require('jasmine-core'); 39 | // module.exports = factory(root, function() { 40 | // return jasmineRequire; 41 | // }); 42 | // } else { 43 | // Browser globals 44 | window.MockAjax = factory(root, getJasmineRequireObj); 45 | // } 46 | }(typeof window !== 'undefined' ? window : global, function (global, getJasmineRequireObj) { 47 | 48 | // 49 | getJasmineRequireObj().ajax = function(jRequire) { 50 | var $ajax = {}; 51 | 52 | $ajax.RequestStub = jRequire.AjaxRequestStub(); 53 | $ajax.RequestTracker = jRequire.AjaxRequestTracker(); 54 | $ajax.StubTracker = jRequire.AjaxStubTracker(); 55 | $ajax.ParamParser = jRequire.AjaxParamParser(); 56 | $ajax.event = jRequire.AjaxEvent(); 57 | $ajax.eventBus = jRequire.AjaxEventBus($ajax.event); 58 | $ajax.fakeRequest = jRequire.AjaxFakeRequest($ajax.eventBus); 59 | $ajax.MockAjax = jRequire.MockAjax($ajax); 60 | 61 | return $ajax.MockAjax; 62 | }; 63 | 64 | getJasmineRequireObj().AjaxEvent = function() { 65 | function now() { 66 | return new Date().getTime(); 67 | } 68 | 69 | function noop() { 70 | } 71 | 72 | // Event object 73 | // https://dom.spec.whatwg.org/#concept-event 74 | function XMLHttpRequestEvent(xhr, type) { 75 | this.type = type; 76 | this.bubbles = false; 77 | this.cancelable = false; 78 | this.timeStamp = now(); 79 | 80 | this.isTrusted = false; 81 | this.defaultPrevented = false; 82 | 83 | // Event phase should be "AT_TARGET" 84 | // https://dom.spec.whatwg.org/#dom-event-at_target 85 | this.eventPhase = 2; 86 | 87 | this.target = xhr; 88 | this.currentTarget = xhr; 89 | } 90 | 91 | XMLHttpRequestEvent.prototype.preventDefault = noop; 92 | XMLHttpRequestEvent.prototype.stopPropagation = noop; 93 | XMLHttpRequestEvent.prototype.stopImmediatePropagation = noop; 94 | 95 | function XMLHttpRequestProgressEvent() { 96 | XMLHttpRequestEvent.apply(this, arguments); 97 | 98 | this.lengthComputable = false; 99 | this.loaded = 0; 100 | this.total = 0; 101 | } 102 | 103 | // Extend prototype 104 | XMLHttpRequestProgressEvent.prototype = XMLHttpRequestEvent.prototype; 105 | 106 | return { 107 | event: function(xhr, type) { 108 | return new XMLHttpRequestEvent(xhr, type); 109 | }, 110 | 111 | progressEvent: function(xhr, type) { 112 | return new XMLHttpRequestProgressEvent(xhr, type); 113 | } 114 | }; 115 | }; 116 | getJasmineRequireObj().AjaxEventBus = function(eventFactory) { 117 | function EventBus(source) { 118 | this.eventList = {}; 119 | this.source = source; 120 | } 121 | 122 | function ensureEvent(eventList, name) { 123 | eventList[name] = eventList[name] || []; 124 | return eventList[name]; 125 | } 126 | 127 | function findIndex(list, thing) { 128 | if (list.indexOf) { 129 | return list.indexOf(thing); 130 | } 131 | 132 | for(var i = 0; i < list.length; i++) { 133 | if (thing === list[i]) { 134 | return i; 135 | } 136 | } 137 | 138 | return -1; 139 | } 140 | 141 | EventBus.prototype.addEventListener = function(event, callback) { 142 | ensureEvent(this.eventList, event).push(callback); 143 | }; 144 | 145 | EventBus.prototype.removeEventListener = function(event, callback) { 146 | var index = findIndex(this.eventList[event], callback); 147 | 148 | if (index >= 0) { 149 | this.eventList[event].splice(index, 1); 150 | } 151 | }; 152 | 153 | EventBus.prototype.trigger = function(event) { 154 | var evt; 155 | 156 | // Event 'readystatechange' is should be a simple event. 157 | // Others are progress event. 158 | // https://xhr.spec.whatwg.org/#events 159 | if (event === 'readystatechange') { 160 | evt = eventFactory.event(this.source, event); 161 | } else { 162 | evt = eventFactory.progressEvent(this.source, event); 163 | } 164 | 165 | var eventListeners = this.eventList[event]; 166 | 167 | if (eventListeners) { 168 | for (var i = 0; i < eventListeners.length; i++) { 169 | eventListeners[i].call(this.source, evt); 170 | } 171 | } 172 | }; 173 | 174 | return function(source) { 175 | return new EventBus(source); 176 | }; 177 | }; 178 | 179 | getJasmineRequireObj().AjaxFakeRequest = function(eventBusFactory) { 180 | function extend(destination, source, propertiesToSkip) { 181 | propertiesToSkip = propertiesToSkip || []; 182 | for (var property in source) { 183 | if (!arrayContains(propertiesToSkip, property)) { 184 | destination[property] = source[property]; 185 | } 186 | } 187 | return destination; 188 | } 189 | 190 | function arrayContains(arr, item) { 191 | for (var i = 0; i < arr.length; i++) { 192 | if (arr[i] === item) { 193 | return true; 194 | } 195 | } 196 | return false; 197 | } 198 | 199 | function wrapProgressEvent(xhr, eventName) { 200 | return function() { 201 | if (xhr[eventName]) { 202 | xhr[eventName].apply(xhr, arguments); 203 | } 204 | }; 205 | } 206 | 207 | function initializeEvents(xhr) { 208 | xhr.eventBus.addEventListener('readystatechange', wrapProgressEvent(xhr, 'onreadystatechange')); 209 | xhr.eventBus.addEventListener('loadstart', wrapProgressEvent(xhr, 'onloadstart')); 210 | xhr.eventBus.addEventListener('load', wrapProgressEvent(xhr, 'onload')); 211 | xhr.eventBus.addEventListener('loadend', wrapProgressEvent(xhr, 'onloadend')); 212 | xhr.eventBus.addEventListener('progress', wrapProgressEvent(xhr, 'onprogress')); 213 | xhr.eventBus.addEventListener('error', wrapProgressEvent(xhr, 'onerror')); 214 | xhr.eventBus.addEventListener('abort', wrapProgressEvent(xhr, 'onabort')); 215 | xhr.eventBus.addEventListener('timeout', wrapProgressEvent(xhr, 'ontimeout')); 216 | } 217 | 218 | function unconvertibleResponseTypeMessage(type) { 219 | var msg = [ 220 | "Can't build XHR.response for XHR.responseType of '", 221 | type, 222 | "'.", 223 | "XHR.response must be explicitly stubbed" 224 | ]; 225 | return msg.join(' '); 226 | } 227 | 228 | function fakeRequest(global, requestTracker, stubTracker, paramParser) { 229 | function FakeXMLHttpRequest() { 230 | requestTracker.track(this); 231 | this.eventBus = eventBusFactory(this); 232 | initializeEvents(this); 233 | this.requestHeaders = {}; 234 | this.overriddenMimeType = null; 235 | } 236 | 237 | function findHeader(name, headers) { 238 | name = name.toLowerCase(); 239 | for (var header in headers) { 240 | if (header.toLowerCase() === name) { 241 | return headers[header]; 242 | } 243 | } 244 | } 245 | 246 | function normalizeHeaders(rawHeaders, contentType) { 247 | var headers = []; 248 | 249 | if (rawHeaders) { 250 | if (rawHeaders instanceof Array) { 251 | headers = rawHeaders; 252 | } else { 253 | for (var headerName in rawHeaders) { 254 | if (rawHeaders.hasOwnProperty(headerName)) { 255 | headers.push({ name: headerName, value: rawHeaders[headerName] }); 256 | } 257 | } 258 | } 259 | } else { 260 | headers.push({ name: "Content-Type", value: contentType || "application/json" }); 261 | } 262 | 263 | return headers; 264 | } 265 | 266 | function parseXml(xmlText, contentType) { 267 | if (global.DOMParser) { 268 | return (new global.DOMParser()).parseFromString(xmlText, 'text/xml'); 269 | } else { 270 | var xml = new global.ActiveXObject("Microsoft.XMLDOM"); 271 | xml.async = "false"; 272 | xml.loadXML(xmlText); 273 | return xml; 274 | } 275 | } 276 | 277 | var xmlParsables = ['text/xml', 'application/xml']; 278 | 279 | function getResponseXml(responseText, contentType) { 280 | if (arrayContains(xmlParsables, contentType.toLowerCase())) { 281 | return parseXml(responseText, contentType); 282 | } else if (contentType.match(/\+xml$/)) { 283 | return parseXml(responseText, 'text/xml'); 284 | } 285 | return null; 286 | } 287 | 288 | extend(FakeXMLHttpRequest, { 289 | UNSENT: 0, 290 | OPENED: 1, 291 | HEADERS_RECEIVED: 2, 292 | LOADING: 3, 293 | DONE: 4 294 | }); 295 | 296 | var iePropertiesThatCannotBeCopied = ['responseBody', 'responseText', 'responseXML', 'status', 'statusText', 'responseTimeout', 'responseURL']; 297 | extend(FakeXMLHttpRequest.prototype, new global.XMLHttpRequest(), iePropertiesThatCannotBeCopied); 298 | extend(FakeXMLHttpRequest.prototype, { 299 | open: function() { 300 | this.method = arguments[0]; 301 | this.url = arguments[1] + ''; 302 | this.username = arguments[3]; 303 | this.password = arguments[4]; 304 | this.readyState = FakeXMLHttpRequest.OPENED; 305 | this.requestHeaders = {}; 306 | this.eventBus.trigger('readystatechange'); 307 | }, 308 | 309 | setRequestHeader: function(header, value) { 310 | if (this.readyState === 0) { 311 | throw new Error('DOMException: Failed to execute "setRequestHeader" on "XMLHttpRequest": The object\'s state must be OPENED.'); 312 | } 313 | 314 | if(this.requestHeaders.hasOwnProperty(header)) { 315 | this.requestHeaders[header] = [this.requestHeaders[header], value].join(', '); 316 | } else { 317 | this.requestHeaders[header] = value; 318 | } 319 | }, 320 | 321 | overrideMimeType: function(mime) { 322 | this.overriddenMimeType = mime; 323 | }, 324 | 325 | abort: function() { 326 | this.readyState = FakeXMLHttpRequest.UNSENT; 327 | this.status = 0; 328 | this.statusText = "abort"; 329 | this.eventBus.trigger('readystatechange'); 330 | this.eventBus.trigger('progress'); 331 | this.eventBus.trigger('abort'); 332 | this.eventBus.trigger('loadend'); 333 | }, 334 | 335 | readyState: FakeXMLHttpRequest.UNSENT, 336 | 337 | onloadstart: null, 338 | onprogress: null, 339 | onabort: null, 340 | onerror: null, 341 | onload: null, 342 | ontimeout: null, 343 | onloadend: null, 344 | onreadystatechange: null, 345 | 346 | addEventListener: function() { 347 | this.eventBus.addEventListener.apply(this.eventBus, arguments); 348 | }, 349 | 350 | removeEventListener: function(event, callback) { 351 | this.eventBus.removeEventListener.apply(this.eventBus, arguments); 352 | }, 353 | 354 | status: null, 355 | 356 | send: function(data) { 357 | this.params = data; 358 | this.eventBus.trigger('loadstart'); 359 | 360 | var stub = stubTracker.findStub(this.url, data, this.method); 361 | if (stub) { 362 | stub.handleRequest(this); 363 | } 364 | }, 365 | 366 | contentType: function() { 367 | return findHeader('content-type', this.requestHeaders); 368 | }, 369 | 370 | data: function() { 371 | if (!this.params) { 372 | return {}; 373 | } 374 | 375 | return paramParser.findParser(this).parse(this.params); 376 | }, 377 | 378 | getResponseHeader: function(name) { 379 | var resultHeader = null; 380 | if (!this.responseHeaders) { return resultHeader; } 381 | 382 | name = name.toLowerCase(); 383 | for(var i = 0; i < this.responseHeaders.length; i++) { 384 | var header = this.responseHeaders[i]; 385 | if (name === header.name.toLowerCase()) { 386 | if (resultHeader) { 387 | resultHeader = [resultHeader, header.value].join(', '); 388 | } else { 389 | resultHeader = header.value; 390 | } 391 | } 392 | } 393 | return resultHeader; 394 | }, 395 | 396 | getAllResponseHeaders: function() { 397 | if (!this.responseHeaders) { return null; } 398 | 399 | var responseHeaders = []; 400 | for (var i = 0; i < this.responseHeaders.length; i++) { 401 | responseHeaders.push(this.responseHeaders[i].name + ': ' + 402 | this.responseHeaders[i].value); 403 | } 404 | return responseHeaders.join('\r\n') + '\r\n'; 405 | }, 406 | 407 | responseText: null, 408 | response: null, 409 | responseType: null, 410 | responseURL: null, 411 | 412 | responseValue: function() { 413 | switch(this.responseType) { 414 | case null: 415 | case "": 416 | case "text": 417 | return this.readyState >= FakeXMLHttpRequest.LOADING ? this.responseText : ""; 418 | case "json": 419 | return JSON.parse(this.responseText); 420 | case "arraybuffer": 421 | throw unconvertibleResponseTypeMessage('arraybuffer'); 422 | case "blob": 423 | throw unconvertibleResponseTypeMessage('blob'); 424 | case "document": 425 | return this.responseXML; 426 | } 427 | }, 428 | 429 | 430 | respondWith: function(response) { 431 | if (this.readyState === FakeXMLHttpRequest.DONE) { 432 | throw new Error("FakeXMLHttpRequest already completed"); 433 | } 434 | 435 | this.status = response.status; 436 | this.statusText = response.statusText || ""; 437 | this.responseHeaders = normalizeHeaders(response.responseHeaders, response.contentType); 438 | this.readyState = FakeXMLHttpRequest.HEADERS_RECEIVED; 439 | this.eventBus.trigger('readystatechange'); 440 | 441 | this.responseText = response.responseText || ""; 442 | this.responseType = response.responseType || ""; 443 | this.responseURL = response.responseURL || null; 444 | this.readyState = FakeXMLHttpRequest.DONE; 445 | this.responseXML = getResponseXml(response.responseText, this.getResponseHeader('content-type') || ''); 446 | if (this.responseXML) { 447 | this.responseType = 'document'; 448 | } 449 | if (response.responseJSON) { 450 | this.responseText = JSON.stringify(response.responseJSON); 451 | } 452 | 453 | if ('response' in response) { 454 | this.response = response.response; 455 | } else { 456 | this.response = this.responseValue(); 457 | } 458 | 459 | this.eventBus.trigger('readystatechange'); 460 | this.eventBus.trigger('progress'); 461 | this.eventBus.trigger('load'); 462 | this.eventBus.trigger('loadend'); 463 | }, 464 | 465 | responseTimeout: function() { 466 | if (this.readyState === FakeXMLHttpRequest.DONE) { 467 | throw new Error("FakeXMLHttpRequest already completed"); 468 | } 469 | this.readyState = FakeXMLHttpRequest.DONE; 470 | jasmine.clock().tick(30000); 471 | this.eventBus.trigger('readystatechange'); 472 | this.eventBus.trigger('progress'); 473 | this.eventBus.trigger('timeout'); 474 | this.eventBus.trigger('loadend'); 475 | }, 476 | 477 | responseError: function(response) { 478 | if (!response) { 479 | response = {}; 480 | } 481 | if (this.readyState === FakeXMLHttpRequest.DONE) { 482 | throw new Error("FakeXMLHttpRequest already completed"); 483 | } 484 | this.status = response.status; 485 | this.statusText = response.statusText || ""; 486 | this.readyState = FakeXMLHttpRequest.DONE; 487 | this.eventBus.trigger('readystatechange'); 488 | this.eventBus.trigger('progress'); 489 | this.eventBus.trigger('error'); 490 | this.eventBus.trigger('loadend'); 491 | }, 492 | 493 | startStream: function(options) { 494 | if (!options) { 495 | options = {}; 496 | } 497 | 498 | if (this.readyState >= FakeXMLHttpRequest.LOADING) { 499 | throw new Error("FakeXMLHttpRequest already loading or finished"); 500 | } 501 | 502 | this.status = 200; 503 | this.responseText = ""; 504 | this.statusText = ""; 505 | 506 | this.responseHeaders = normalizeHeaders(options.responseHeaders, options.contentType); 507 | this.readyState = FakeXMLHttpRequest.HEADERS_RECEIVED; 508 | this.eventBus.trigger('readystatechange'); 509 | 510 | this.responseType = options.responseType || ""; 511 | this.responseURL = options.responseURL || null; 512 | this.readyState = FakeXMLHttpRequest.LOADING; 513 | this.eventBus.trigger('readystatechange'); 514 | }, 515 | 516 | streamData: function(data) { 517 | if (this.readyState !== FakeXMLHttpRequest.LOADING) { 518 | throw new Error("FakeXMLHttpRequest is not loading yet"); 519 | } 520 | 521 | this.responseText += data; 522 | this.responseXML = getResponseXml(this.responseText, this.getResponseHeader('content-type') || ''); 523 | if (this.responseXML) { 524 | this.responseType = 'document'; 525 | } 526 | 527 | this.response = this.responseValue(); 528 | 529 | this.eventBus.trigger('readystatechange'); 530 | this.eventBus.trigger('progress'); 531 | }, 532 | 533 | cancelStream: function () { 534 | if (this.readyState === FakeXMLHttpRequest.DONE) { 535 | throw new Error("FakeXMLHttpRequest already completed"); 536 | } 537 | 538 | this.status = 0; 539 | this.statusText = ""; 540 | this.readyState = FakeXMLHttpRequest.DONE; 541 | this.eventBus.trigger('readystatechange'); 542 | this.eventBus.trigger('progress'); 543 | this.eventBus.trigger('loadend'); 544 | }, 545 | 546 | completeStream: function(status) { 547 | if (this.readyState === FakeXMLHttpRequest.DONE) { 548 | throw new Error("FakeXMLHttpRequest already completed"); 549 | } 550 | 551 | this.status = status || 200; 552 | this.statusText = ""; 553 | this.readyState = FakeXMLHttpRequest.DONE; 554 | this.eventBus.trigger('readystatechange'); 555 | this.eventBus.trigger('progress'); 556 | this.eventBus.trigger('loadend'); 557 | } 558 | }); 559 | 560 | return FakeXMLHttpRequest; 561 | } 562 | 563 | return fakeRequest; 564 | }; 565 | 566 | getJasmineRequireObj().MockAjax = function($ajax) { 567 | function MockAjax(global) { 568 | var requestTracker = new $ajax.RequestTracker(), 569 | stubTracker = new $ajax.StubTracker(), 570 | paramParser = new $ajax.ParamParser(), 571 | realAjaxFunction = global.XMLHttpRequest, 572 | mockAjaxFunction = $ajax.fakeRequest(global, requestTracker, stubTracker, paramParser); 573 | 574 | this.install = function() { 575 | if (global.XMLHttpRequest !== realAjaxFunction) { 576 | throw new Error("Jasmine Ajax was unable to install over a custom XMLHttpRequest. Is Jasmine Ajax already installed?"); 577 | } 578 | 579 | global.XMLHttpRequest = mockAjaxFunction; 580 | }; 581 | 582 | this.uninstall = function() { 583 | if (global.XMLHttpRequest !== mockAjaxFunction) { 584 | throw new Error("MockAjax not installed."); 585 | } 586 | global.XMLHttpRequest = realAjaxFunction; 587 | 588 | this.stubs.reset(); 589 | this.requests.reset(); 590 | paramParser.reset(); 591 | }; 592 | 593 | this.stubRequest = function(url, data, method) { 594 | var stub = new $ajax.RequestStub(url, data, method); 595 | stubTracker.addStub(stub); 596 | return stub; 597 | }; 598 | 599 | this.withMock = function(closure) { 600 | this.install(); 601 | try { 602 | closure(); 603 | } finally { 604 | this.uninstall(); 605 | } 606 | }; 607 | 608 | this.addCustomParamParser = function(parser) { 609 | paramParser.add(parser); 610 | }; 611 | 612 | this.requests = requestTracker; 613 | this.stubs = stubTracker; 614 | } 615 | 616 | return MockAjax; 617 | }; 618 | 619 | getJasmineRequireObj().AjaxParamParser = function() { 620 | function ParamParser() { 621 | var defaults = [ 622 | { 623 | test: function(xhr) { 624 | return (/^application\/json/).test(xhr.contentType()); 625 | }, 626 | parse: function jsonParser(paramString) { 627 | return JSON.parse(paramString); 628 | } 629 | }, 630 | { 631 | test: function(xhr) { 632 | return true; 633 | }, 634 | parse: function naiveParser(paramString) { 635 | var data = {}; 636 | var params = paramString.split('&'); 637 | 638 | for (var i = 0; i < params.length; ++i) { 639 | var kv = params[i].replace(/\+/g, ' ').split('='); 640 | var key = decodeURIComponent(kv[0]); 641 | data[key] = data[key] || []; 642 | data[key].push(decodeURIComponent(kv[1])); 643 | } 644 | return data; 645 | } 646 | } 647 | ]; 648 | var paramParsers = []; 649 | 650 | this.add = function(parser) { 651 | paramParsers.unshift(parser); 652 | }; 653 | 654 | this.findParser = function(xhr) { 655 | for(var i in paramParsers) { 656 | var parser = paramParsers[i]; 657 | if (parser.test(xhr)) { 658 | return parser; 659 | } 660 | } 661 | }; 662 | 663 | this.reset = function() { 664 | paramParsers = []; 665 | for(var i in defaults) { 666 | paramParsers.push(defaults[i]); 667 | } 668 | }; 669 | 670 | this.reset(); 671 | } 672 | 673 | return ParamParser; 674 | }; 675 | 676 | getJasmineRequireObj().AjaxRequestStub = function() { 677 | var RETURN = 0, 678 | ERROR = 1, 679 | TIMEOUT = 2, 680 | CALL = 3; 681 | 682 | var normalizeQuery = function(query) { 683 | return query ? query.split('&').sort().join('&') : undefined; 684 | }; 685 | 686 | var timeoutRequest = function(request) { 687 | request.responseTimeout(); 688 | }; 689 | 690 | function RequestStub(url, stubData, method) { 691 | if (url instanceof RegExp) { 692 | this.url = url; 693 | this.query = undefined; 694 | } else { 695 | var split = url.split('?'); 696 | this.url = split[0]; 697 | this.query = split.length > 1 ? normalizeQuery(split[1]) : undefined; 698 | } 699 | 700 | this.data = (stubData instanceof RegExp) ? stubData : normalizeQuery(stubData); 701 | this.method = method; 702 | } 703 | 704 | RequestStub.prototype = { 705 | andReturn: function(options) { 706 | options.status = (typeof options.status !== 'undefined') ? options.status : 200; 707 | this.handleRequest = function(request) { 708 | request.respondWith(options); 709 | }; 710 | }, 711 | 712 | andError: function(options) { 713 | if (!options) { 714 | options = {}; 715 | } 716 | options.status = options.status || 500; 717 | this.handleRequest = function(request) { 718 | request.responseError(options); 719 | }; 720 | }, 721 | 722 | andTimeout: function() { 723 | this.handleRequest = timeoutRequest; 724 | }, 725 | 726 | andCallFunction: function(functionToCall) { 727 | this.handleRequest = function(request) { 728 | functionToCall(request); 729 | }; 730 | }, 731 | 732 | matches: function(fullUrl, data, method) { 733 | var urlMatches = false; 734 | fullUrl = fullUrl.toString(); 735 | if (this.url instanceof RegExp) { 736 | urlMatches = this.url.test(fullUrl); 737 | } else { 738 | var urlSplit = fullUrl.split('?'), 739 | url = urlSplit[0], 740 | query = urlSplit[1]; 741 | urlMatches = this.url === url && this.query === normalizeQuery(query); 742 | } 743 | var dataMatches = false; 744 | if (this.data instanceof RegExp) { 745 | dataMatches = this.data.test(data); 746 | } else { 747 | dataMatches = !this.data || this.data === normalizeQuery(data); 748 | } 749 | return urlMatches && dataMatches && (!this.method || this.method === method); 750 | } 751 | }; 752 | 753 | return RequestStub; 754 | }; 755 | 756 | getJasmineRequireObj().AjaxRequestTracker = function() { 757 | function RequestTracker() { 758 | var requests = []; 759 | 760 | this.track = function(request) { 761 | requests.push(request); 762 | }; 763 | 764 | this.first = function() { 765 | return requests[0]; 766 | }; 767 | 768 | this.count = function() { 769 | return requests.length; 770 | }; 771 | 772 | this.reset = function() { 773 | requests = []; 774 | }; 775 | 776 | this.mostRecent = function() { 777 | return requests[requests.length - 1]; 778 | }; 779 | 780 | this.at = function(index) { 781 | return requests[index]; 782 | }; 783 | 784 | this.filter = function(url_to_match) { 785 | var matching_requests = []; 786 | 787 | for (var i = 0; i < requests.length; i++) { 788 | if (url_to_match instanceof RegExp && 789 | url_to_match.test(requests[i].url)) { 790 | matching_requests.push(requests[i]); 791 | } else if (url_to_match instanceof Function && 792 | url_to_match(requests[i])) { 793 | matching_requests.push(requests[i]); 794 | } else { 795 | if (requests[i].url === url_to_match) { 796 | matching_requests.push(requests[i]); 797 | } 798 | } 799 | } 800 | 801 | return matching_requests; 802 | }; 803 | } 804 | 805 | return RequestTracker; 806 | }; 807 | 808 | getJasmineRequireObj().AjaxStubTracker = function() { 809 | function StubTracker() { 810 | var stubs = []; 811 | 812 | this.addStub = function(stub) { 813 | stubs.push(stub); 814 | }; 815 | 816 | this.reset = function() { 817 | stubs = []; 818 | }; 819 | 820 | this.findStub = function(url, data, method) { 821 | for (var i = stubs.length - 1; i >= 0; i--) { 822 | var stub = stubs[i]; 823 | if (stub.matches(url, data, method)) { 824 | return stub; 825 | } 826 | } 827 | }; 828 | } 829 | 830 | return StubTracker; 831 | }; 832 | 833 | 834 | var jRequire = getJasmineRequireObj(); 835 | var MockAjax = jRequire.ajax(jRequire); 836 | jasmine.Ajax = new MockAjax(global); 837 | 838 | return MockAjax; 839 | })); 840 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jasmine-ajax", 3 | "description": "A library for faking Ajax responses in your Jasmine suite", 4 | "version": "4.0.0", 5 | "main": "lib/mock-ajax.js", 6 | "license": "MIT", 7 | "url": "https://github.com/jasmine/jasmine-ajax", 8 | "contributors": [ 9 | "Gregg Van Hove ", 10 | "JR Boyens " 11 | ], 12 | "private": false, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/jasmine/jasmine-ajax" 16 | }, 17 | "devDependencies": { 18 | "fast-glob": "^3.2.5", 19 | "grunt": "^1.4.1", 20 | "grunt-cli": "~0.1.13", 21 | "grunt-contrib-jshint": "^3.0.0", 22 | "grunt-includes": "^0.5.1", 23 | "grunt-shell": "^3.0.1", 24 | "grunt-template": "^1.0.0", 25 | "jasmine-browser-runner": "^3.0.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /release_notes/2.0.2.md: -------------------------------------------------------------------------------- 1 | # Jasmine-Ajax 2.0.2 Release Notes 2 | 3 | ## Summary 4 | 5 | Release of jasmine-ajax with many fixes to be more compatible with the full XHR spec. 6 | 7 | ## Changes 8 | 9 | - Combine request headers with the same name 10 | - Find response case-insensitively 11 | - Split into multiple files for development 12 | - We use the `getJasmineRequireObj()` for loading dependencies 13 | - CI runs specs against both separate and concatenated files 14 | 15 | ## Pull Requests and Issues 16 | - Allow progress event callbacks to be null instead of just empty functions. 17 | - Fixes [#85](http://github.com/pivotal/jasmine-ajax/issues/85) 18 | 19 | - Support listening to XHR progress events via `addEventListener` 20 | - Fixes [#79](http://github.com/pivotal/jasmine-ajax/issues/79) 21 | 22 | - Support all XHR events 23 | - Fixes [#81](http://github.com/pivotal/jasmine-ajax/issues/81) 24 | - Fixes [#82](http://github.com/pivotal/jasmine-ajax/issues/82) 25 | 26 | - Parse `responseText` into a `responseXML` if the contentType is xml-ish 27 | - Fixes [#55](http://github.com/pivotal/jasmine-ajax/issues/55) 28 | 29 | - Un-pin jasmine dependency from version 2.0.0 30 | - Merges [#76](http://github.com/pivotal/jasmine-ajax/issues/76) from @macroeyes 31 | 32 | - added support for multiple headers with same name 33 | - Merges [#72](http://github.com/pivotal/jasmine-ajax/issues/72) from @resident-uhlig 34 | 35 | - Record overridden mime types 36 | - Fixes [#74](http://github.com/pivotal/jasmine-ajax/issues/74) 37 | 38 | - Throw an error if response() is called more than once on requests 39 | - Fixes [#70](http://github.com/pivotal/jasmine-ajax/issues/70) 40 | - Merges [#71](http://github.com/pivotal/jasmine-ajax/issues/71) from @kmontag 41 | 42 | - Allow stubs to match only a particular http method (GET, POST, PUT, etc.) 43 | - Fixes [#36](http://github.com/pivotal/jasmine-ajax/issues/36) 44 | 45 | - Add some files to the bower.json ignore list 46 | - Merges [#69](http://github.com/pivotal/jasmine-ajax/issues/69 ) from @lencioni 47 | 48 | ------ 49 | 50 | _Release Notes generated with _[Anchorman](http://github.com/infews/anchorman)_ 51 | -------------------------------------------------------------------------------- /release_notes/2.99.md: -------------------------------------------------------------------------------- 1 | # Jasmine-Ajax 2.99 Release Notes 2 | 3 | ## Summary 4 | 5 | Release of jasmine-ajax to provide a non-breaking upgrade path as we move to support XHR 2. 6 | 7 | ## Changes 8 | 9 | - Deprecate the `response` function on fake request in favor of `respondWith`. 10 | 11 | 12 | ------ 13 | 14 | _Release Notes generated with _[Anchorman](http://github.com/infews/anchorman)_ 15 | -------------------------------------------------------------------------------- /release_notes/3.0.md: -------------------------------------------------------------------------------- 1 | # Jasmine-Ajax 3.0 Release Notes 2 | 3 | ## Summary 4 | 5 | Support XHR 2 6 | 7 | This release contains a breaking change. The `response` method is no longer supported, you should use `respondWith` instead. 8 | 9 | ## Changes 10 | 11 | * Add grunt tasks for releasing packages 12 | * Bring npm and bower configurations in line with each other 13 | * Also test in IE 11 on travis 14 | 15 | ## Pull Requests & Issues 16 | 17 | * Include trailing newline in response headers to match browsers. 18 | - Fixes [#91](https://github.com/jasmine/jasmine-ajax/issues/91) 19 | 20 | * modify example code 'response' method is deprecated. use 'respondWith' instead 21 | - Merges [#87](https://github.com/jasmine/jasmine-ajax/issues/87) from @Mstudio-Ishikawa 22 | 23 | * Support for XHR 2 24 | - Merges [#60](https://github.com/jasmine/jasmine-ajax/issues/60) from @albertyw 25 | 26 | ------ 27 | 28 | _Release Notes generated with _[Anchorman](http://github.com/infews/anchorman)_ 29 | -------------------------------------------------------------------------------- /release_notes/3.1.0.md: -------------------------------------------------------------------------------- 1 | # Jasmine-Ajax 3.1 Release Notes 2 | 3 | ## Summary 4 | 5 | This is a maintenance release. 6 | 7 | ## Changes 8 | 9 | * Add safari 7 & 8 to browser matrix 10 | 11 | ## Pull Requests & Issues 12 | 13 | * Add version number to combined file 14 | - Fixes #93 15 | 16 | * Implement `removeEventListener` 17 | - Merges #89 from @illiptic 18 | 19 | * Add `responseHeaders` to `andReturn` s options 20 | - Merges #94 from @tomalec 21 | 22 | ------ 23 | 24 | _Release Notes generated with _[Anchorman](http://github.com/infews/anchorman)_ 25 | -------------------------------------------------------------------------------- /release_notes/3.1.1.md: -------------------------------------------------------------------------------- 1 | # Jasmine-Ajax 3.1 Release Notes 2 | 3 | ## Pull Requests & Issues 4 | 5 | * Loosen bower dependency on jasmine 6 | - Fixes [#106](https://github.com/jasmine/jasmine-ajax/issues/106) 7 | 8 | * Event readystatechange is triggered when headers are available 9 | - Merges [#104](https://github.com/jasmine/jasmine-ajax/issues/104) from @mjeanroy 10 | - Fixes [#97](https://github.com/jasmine/jasmine-ajax/issues/97) 11 | - Fixes [#103](https://github.com/jasmine/jasmine-ajax/issues/103) 12 | - Fixes [#102](https://github.com/jasmine/jasmine-ajax/issues/102) 13 | 14 | * Throw error on duplicate calls to MockAjax#install. 15 | - Merges [#98](https://github.com/jasmine/jasmine-ajax/issues/98) from @jhamon 16 | - Fixes [#96](https://github.com/jasmine/jasmine-ajax/issues/96) 17 | 18 | * Readme tweaks and updated links 19 | - Merges [#99](https://github.com/jasmine/jasmine-ajax/issues/99) from @jhamon 20 | 21 | ------ 22 | 23 | _Release Notes generated with _[Anchorman](http://github.com/infews/anchorman)_ 24 | -------------------------------------------------------------------------------- /release_notes/3.2.0.md: -------------------------------------------------------------------------------- 1 | # Jasmine-Ajax 3.2.0 Release Notes 2 | 3 | ## Pull Requests & Issues 4 | 5 | * Set the `this` context for event handler to the current XHR and pass proper ProgressEvent arguments 6 | - Merges [#114](https://github.com/jasmine/jasmine-ajax/issues/114) from @mjeanroy 7 | - Fixes [#108](https://github.com/jasmine/jasmine-ajax/issues/108) 8 | 9 | * added feature to match data/query against RegExp 10 | - Merges [#116](https://github.com/jasmine/jasmine-ajax/issues/116) from @SunboX 11 | - Fixes [#115](https://github.com/jasmine/jasmine-ajax/issues/115) 12 | 13 | * Allow error and timeout using RequestStub 14 | - Merges [#112](https://github.com/jasmine/jasmine-ajax/issues/112) from @Mordred 15 | - Fixes [#111](https://github.com/jasmine/jasmine-ajax/issues/111) 16 | 17 | 18 | ------ 19 | 20 | _Release Notes generated with _[Anchorman](http://github.com/infews/anchorman)_ 21 | -------------------------------------------------------------------------------- /release_notes/3.3.0.md: -------------------------------------------------------------------------------- 1 | # Jasmine-Ajax 3.3.0 Release Notes 2 | 3 | ## Summary 4 | 5 | This is a general release with a number of new features for Jasmine-Ajax 6 | 7 | ## Pull Requests & Issues 8 | 9 | * Fix typo in README 10 | - Merges #166 from @randoum 11 | 12 | * FakeXMLHttpRequest.getResponseHeader returns null on missing headers. 13 | - Merges #163 from @apepper 14 | - Fixes #153 15 | 16 | * The XMLHttpRequest must be opened before to set the headers 17 | - Merges #160 from @DarioG 18 | 19 | * Add static state types 20 | - Merges #158 from @gweax 21 | 22 | * Fix README text to specify the correct number of parts 23 | - Merges #162 @RickCarlino 24 | 25 | * Add `RequestStub.andCallFunction`. 26 | - Merges #152 from @kring 27 | 28 | * Wrap code in module wrapper to support both Browser and CommonJS environment 29 | - Merges #140 from @just-boris 30 | - Closes #137 31 | - Fixes #95 32 | 33 | * Clear request headers on fakeRequest.open() 34 | - Merges #147 from @mooglemoogle 35 | - Fixes #142 36 | 37 | * Add responseURL support 38 | - Merges #146 from @joeyparrish 39 | - Fixes #145 40 | 41 | * Add NPM installation instructions 42 | - Merges #135 from @jonnyreeves 43 | - Merges #136 from @jonnyreeves 44 | - Fixes #131 45 | 46 | * Throw exception on multiple calls to jasmine.Ajax.uninstall. 47 | - Merges #128 from @salticus 48 | 49 | * Update messaging for 1.3 compatibility 50 | - Fixes #121 51 | 52 | ------ 53 | 54 | _Release Notes generated with _[Anchorman](http://github.com/infews/anchorman)_ 55 | -------------------------------------------------------------------------------- /release_notes/3.3.1.md: -------------------------------------------------------------------------------- 1 | # Jasmine-Ajax 3.3.1 Release Notes 2 | 3 | ## Summary 4 | 5 | This is a patch release to restore functionality for CommonJS, but non-nodejs environments 6 | 7 | ## Issues 8 | 9 | * No longer try to detect CommonJS to `require` core 10 | - Fixes #155 11 | - Fixes #168 12 | 13 | ------ 14 | 15 | _Release Notes generated with _[Anchorman](http://github.com/infews/anchorman)_ 16 | -------------------------------------------------------------------------------- /release_notes/3.4.0.md: -------------------------------------------------------------------------------- 1 | # Jasmine Ajax 3.4.0 Release Notes 2 | 3 | ## Summary 4 | This is a general release with a number of new features for Jasmine-Ajax 5 | 6 | ## Changes 7 | 8 | * Stub Requests can pass through `statusText` 9 | - Fixes [#188](https://github.com/jasmine/jasmine-ajax/issues/188) 10 | 11 | * Convert url values to a string to work more like a native XHR 12 | - Fixes [#143](https://github.com/jasmine/jasmine-ajax/issues/143) 13 | 14 | * Check for real XHR instead of mock on install and word error better 15 | - See [#186](https://github.com/jasmine/jasmine-ajax/issues/186) 16 | 17 | * respondWith allows responseJSON property shortcut for stringifing an object 18 | - Merges [#185](https://github.com/jasmine/jasmine-ajax/issues/185) from @k-funk 19 | - Fixes [#177](https://github.com/jasmine/jasmine-ajax/issues/177) 20 | 21 | * Added checklist item for generating the `mock-ajax` file 22 | - See [#184](https://github.com/jasmine/jasmine-ajax/issues/184) 23 | 24 | * Update contributing docs and README to better explain things 25 | - Fixes [#184](https://github.com/jasmine/jasmine-ajax/issues/184) 26 | 27 | * Allow response status 0 for request stubs 28 | - Merges [#182](https://github.com/jasmine/jasmine-ajax/issues/182) from @kiramclean 29 | - Fixes [#141](https://github.com/jasmine/jasmine-ajax/issues/141) 30 | 31 | * Added instructions for contributing. 32 | - Merges [#179](https://github.com/jasmine/jasmine-ajax/issues/179) from @Arthaey 33 | 34 | * Add status and statusText to responseError. Defaults to 500. 35 | - Merges [#176](https://github.com/jasmine/jasmine-ajax/issues/176) from @Arthaey 36 | 37 | * Update readme to suggest creating mock-ajax.js reference in helpers path, not src_files 38 | - Merges [#173](https://github.com/jasmine/jasmine-ajax/issues/173) from @alidaka 39 | 40 | * Add "uninstall" notes to README 41 | - Merges [#169](https://github.com/jasmine/jasmine-ajax/issues/169) from @johnnymugs 42 | 43 | ------ 44 | 45 | _Release Notes generated with _[Anchorman](http://github.com/infews/anchorman)_ 46 | -------------------------------------------------------------------------------- /release_notes/4.0.0.md: -------------------------------------------------------------------------------- 1 | # Jasmine Ajax 4.0.0 Release Notes 2 | 3 | ## Summary 4 | This release include a number of new features as well as a minor breaking change. 5 | 6 | ## Breaking Changes 7 | 8 | * The callback for `andCallFunction` on a request stub, will now only receive the XHR request object as an argument. Previously, it received the stub itself first and the XHR object second. Given the way we expect this to have been used, the request is the more important. 9 | * The `responseCallFunction` on the FakeRequest has also been removed. If you were using that directly and not through the stub functionality, you should be able to replace it by calling your function directly and passing in the request. 10 | 11 | 12 | ## Changes 13 | 14 | * `andCallFunction` on a request stub has more deterministic behavior across multiple calls. See breaking changes 15 | - Fixes [#193](https://github.com/jasmine/jasmine-ajax/issues/193) 16 | 17 | 18 | * Switch RequestStub to use prototype for better memory management 19 | 20 | * Fix link to 'Mocking ajax' page on main Jasmine site 21 | - Merges [#191](https://github.com/jasmine/jasmine-ajax/issues/191) from @koralcem 22 | 23 | 24 | * Added possibility to mock streaming requests 25 | - Merges [#183](https://github.com/jasmine/jasmine-ajax/issues/183) from @timocov 26 | - Fixes [#181](https://github.com/jasmine/jasmine-ajax/issues/181) 27 | 28 | 29 | ------ 30 | 31 | _Release Notes generated with _[Anchorman](http://github.com/infews/anchorman)_ 32 | -------------------------------------------------------------------------------- /scripts/ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | set -e 4 | ./node_modules/.bin/grunt jshint 5 | 6 | # Run tests against all supported browsers 7 | # (see .circleci/config.yml for that) 8 | export SAUCE_TUNNEL_NAME=$CIRCLE_WORKFLOW_JOB_ID 9 | scripts/start-sauce-connect 10 | set +o errexit 11 | scripts/run-all-browsers 12 | exitcode=$? 13 | set -o errexit 14 | scripts/stop-sauce-connect 15 | exit $exitcode 16 | -------------------------------------------------------------------------------- /scripts/run-all-browsers: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | run_browser() { 4 | browser=$1 5 | version=$2 6 | description="$browser $version" 7 | if [ $version = "latest" ]; then 8 | version="" 9 | fi 10 | 11 | echo 12 | echo 13 | echo "Running $description" 14 | echo 15 | USE_SAUCE=true JASMINE_BROWSER=$browser SAUCE_BROWSER_VERSION=$version node scripts/run-in-browser.js 16 | 17 | if [ $? -eq 0 ]; then 18 | echo "PASS: $description" >> "$passfile" 19 | else 20 | echo "FAIL: $description" >> "$failfile" 21 | fi 22 | } 23 | 24 | passfile=`mktemp -t jasmine-results.XXXXXX` || exit 1 25 | failfile=`mktemp -t jasmine-results.XXXXXX` || exit 1 26 | run_browser chrome latest 27 | run_browser firefox latest 28 | run_browser firefox 115 29 | run_browser safari 17 30 | run_browser safari 16 31 | run_browser MicrosoftEdge latest 32 | 33 | echo 34 | cat "$passfile" "$failfile" 35 | 36 | if [ -s "$failfile" ]; then 37 | exit 1 38 | fi 39 | -------------------------------------------------------------------------------- /scripts/run-in-browser.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, es6 */ 2 | const path = require('path'), 3 | jasmineBrowser = require('jasmine-browser-runner'); 4 | 5 | var config = require(path.resolve('spec/support/jasmine-browser.js')); 6 | config.clearReporters = true; 7 | 8 | jasmineBrowser.runSpecs(config).catch(function(error) { 9 | console.error(error); 10 | process.exit(1); 11 | }); 12 | -------------------------------------------------------------------------------- /scripts/start-sauce-connect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o errexit 3 | set -o pipefail 4 | 5 | if [ -z "$SAUCE_TUNNEL_NAME" ]; then 6 | echo "SAUCE_TUNNEL_NAME must be set" 1>&2 7 | exit 1 8 | fi 9 | 10 | outfile=`mktemp` 11 | echo "Starting Sauce Connect" 12 | sc legacy --proxy-localhost --tunnel-domains localhost --region us-west \ 13 | -u "$SAUCE_USERNAME" -k "$SAUCE_ACCESS_KEY" \ 14 | -X 4445 -i "$SAUCE_TUNNEL_NAME" 2>&1 | tee "$outfile" & 15 | 16 | while ! fgrep "Sauce Connect is up, you may start your tests" "$outfile" > /dev/null; do 17 | sleep 1 18 | done 19 | 20 | if ! nc -z localhost 4445; then 21 | echo "Can't connect to Sauce tunnel" 22 | killall sc 23 | exit 1 24 | fi 25 | -------------------------------------------------------------------------------- /scripts/stop-sauce-connect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -o errexit 3 | set -o pipefail 4 | 5 | echo "Stopping Sauce Connect" 6 | 7 | # Sauce Connect 4 docs said that we can just kill -9 it if we don't care about 8 | # failing any ongoing sessions. In practice, that sometimes worked but usually 9 | # leaked a tunnel so badly that you couldn't even stop it from the web UI. 10 | # 11 | # Sauce Connect 5 appears to be *much* more prone to hung jobs. Hung jobs have 12 | # a shutdown deadline of *three hours*, however, they appear to usually exit 13 | # within a minute or so. Unfortunately the only thing the Sauce Connect 5 docs 14 | # say about shutdown is that "you can stop your tunnel from the terminal where 15 | # Sauce Connect is running by entering Ctrl+C". Nothing is said about what to 16 | # do if Sauce Connect doesn't exit on it own or about non-interactive usage. 17 | # 18 | # So we do our best to be well-behaved without assuming that Sauce Connect 19 | # always is: send it the same signal that it would get if an interactive user 20 | # hit ctrl-c, wait a while for it to exit, then give up so that the CI task 21 | # doesn't keep running indefinitely. 22 | 23 | if ! pkill -INT '^sc$'; then 24 | echo "sc does not appear to be running" 1>&2 25 | exit 1 26 | fi 27 | 28 | # Wait up to 2 minutes, then give up if it's still running 29 | n=0 30 | while [ $n -lt 120 ] && pgrep '^sc$' > /dev/null; do 31 | sleep 1 32 | pkill -INT '^sc$' || true 33 | n=$(($n + 1)) 34 | done 35 | 36 | if pgrep '^sc$' > /dev/null; then 37 | echo "Could not shut down Sauce Connect" 38 | exit 1 39 | fi 40 | -------------------------------------------------------------------------------- /spec/eventBusSpec.js: -------------------------------------------------------------------------------- 1 | describe('EventBus', function() { 2 | beforeEach(function() { 3 | var event = this.event = jasmine.createSpyObj('event', [ 4 | 'preventDefault', 5 | 'stopPropagation', 6 | 'stopImmediatePropagation' 7 | ]); 8 | 9 | var progressEvent = this.progressEvent = jasmine.createSpyObj('progressEvent', [ 10 | 'preventDefault', 11 | 'stopPropagation', 12 | 'stopImmediatePropagation' 13 | ]); 14 | 15 | var eventFactory = this.eventFactory = { 16 | event: jasmine.createSpy('event').and.returnValue(event), 17 | progressEvent: jasmine.createSpy('progressEvent').and.returnValue(progressEvent) 18 | }; 19 | 20 | this.xhr = jasmine.createSpy('xhr'); 21 | this.bus = getJasmineRequireObj().AjaxEventBus(eventFactory)(this.xhr); 22 | }); 23 | 24 | it('calls an event listener with event object', function() { 25 | var callback = jasmine.createSpy('callback'); 26 | 27 | this.bus.addEventListener('foo', callback); 28 | this.bus.trigger('foo'); 29 | 30 | expect(callback).toHaveBeenCalledWith(this.progressEvent); 31 | expect(this.eventFactory.progressEvent).toHaveBeenCalledWith(this.xhr, 'foo'); 32 | expect(this.eventFactory.event).not.toHaveBeenCalled(); 33 | }); 34 | 35 | it('calls an readystatechange listener with event object', function() { 36 | var callback = jasmine.createSpy('callback'); 37 | 38 | this.bus.addEventListener('readystatechange', callback); 39 | this.bus.trigger('readystatechange'); 40 | 41 | expect(callback).toHaveBeenCalledWith(this.event); 42 | expect(this.eventFactory.event).toHaveBeenCalledWith(this.xhr, 'readystatechange'); 43 | expect(this.eventFactory.progressEvent).not.toHaveBeenCalled(); 44 | }); 45 | 46 | it('only triggers callbacks for the specified event', function() { 47 | var fooCallback = jasmine.createSpy('foo'), 48 | barCallback = jasmine.createSpy('bar'); 49 | 50 | this.bus.addEventListener('foo', fooCallback); 51 | this.bus.addEventListener('bar', barCallback); 52 | 53 | this.bus.trigger('foo'); 54 | 55 | expect(fooCallback).toHaveBeenCalled(); 56 | expect(barCallback).not.toHaveBeenCalled(); 57 | }); 58 | 59 | it('calls all the callbacks for the specified event', function() { 60 | var callback1 = jasmine.createSpy('callback'); 61 | var callback2 = jasmine.createSpy('otherCallback'); 62 | 63 | this.bus.addEventListener('foo', callback1); 64 | this.bus.addEventListener('foo', callback2); 65 | 66 | this.bus.trigger('foo'); 67 | 68 | expect(callback1).toHaveBeenCalled(); 69 | expect(callback2).toHaveBeenCalled(); 70 | }); 71 | 72 | it('works if there are no callbacks for the event', function() { 73 | var bus = this.bus; 74 | expect(function() { 75 | bus.trigger('notActuallyThere'); 76 | }).not.toThrow(); 77 | }); 78 | 79 | it('does not call listeners that have been removed', function() { 80 | var callback = jasmine.createSpy('callback'); 81 | 82 | this.bus.addEventListener('foo', callback); 83 | this.bus.removeEventListener('foo', callback); 84 | this.bus.trigger('foo'); 85 | 86 | expect(callback).not.toHaveBeenCalled(); 87 | }); 88 | 89 | it('only removes the specified callback', function() { 90 | var callback1 = jasmine.createSpy('callback'); 91 | var callback2 = jasmine.createSpy('otherCallback'); 92 | 93 | this.bus.addEventListener('foo', callback1); 94 | this.bus.addEventListener('foo', callback2); 95 | this.bus.removeEventListener('foo', callback2); 96 | 97 | this.bus.trigger('foo'); 98 | 99 | expect(callback1).toHaveBeenCalled(); 100 | expect(callback2).not.toHaveBeenCalled(); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /spec/eventSpec.js: -------------------------------------------------------------------------------- 1 | describe('Event', function() { 2 | beforeEach(function() { 3 | this.eventFactory = getJasmineRequireObj().AjaxEvent(); 4 | this.xhr = jasmine.createSpy('xhr'); 5 | }); 6 | 7 | it('create an event', function() { 8 | var event = this.eventFactory.event(this.xhr, 'readystatechange'); 9 | 10 | expect(event.type).toBe('readystatechange'); 11 | expect(event.currentTarget).toBe(this.xhr); 12 | expect(event.target).toBe(this.xhr); 13 | expect(event.cancelable).toBe(false); 14 | expect(event.bubbles).toBe(false); 15 | expect(event.defaultPrevented).toBe(false); 16 | expect(event.eventPhase).toBe(2); 17 | expect(event.timeStamp).toBeDefined(); 18 | expect(event.isTrusted).toBe(false); 19 | }); 20 | 21 | it('create a progress event', function() { 22 | var event = this.eventFactory.progressEvent(this.xhr, 'loadend'); 23 | 24 | expect(event.type).toBe('loadend'); 25 | expect(event.currentTarget).toBe(this.xhr); 26 | expect(event.target).toBe(this.xhr); 27 | expect(event.cancelable).toBe(false); 28 | expect(event.bubbles).toBe(false); 29 | expect(event.defaultPrevented).toBe(false); 30 | expect(event.eventPhase).toBe(2); 31 | expect(event.timeStamp).toBeDefined(); 32 | expect(event.isTrusted).toBe(false); 33 | 34 | expect(event.lengthComputable).toBe(false); 35 | expect(event.loaded).toBe(0); 36 | expect(event.total).toBe(0); 37 | }); 38 | }); -------------------------------------------------------------------------------- /spec/fakeRequestSpec.js: -------------------------------------------------------------------------------- 1 | describe('FakeRequest', function() { 2 | beforeEach(function() { 3 | this.requestTracker = { track: jasmine.createSpy('trackRequest') }; 4 | this.stubTracker = { findStub: function() {} }; 5 | var parserInstance = this.parserInstance = jasmine.createSpy('parse'); 6 | this.paramParser = { findParser: function() { return { parse: parserInstance }; } }; 7 | var eventBus = this.fakeEventBus = { 8 | addEventListener: jasmine.createSpy('addEventListener'), 9 | trigger: jasmine.createSpy('trigger'), 10 | removeEventListener: jasmine.createSpy('removeEventListener') 11 | }; 12 | this.eventBusFactory = function() { 13 | return eventBus; 14 | }; 15 | this.fakeGlobal = { 16 | XMLHttpRequest: function() { 17 | this.extraAttribute = 'my cool attribute'; 18 | }, 19 | DOMParser: window.DOMParser, 20 | ActiveXObject: window.ActiveXObject 21 | }; 22 | this.FakeRequest = getJasmineRequireObj().AjaxFakeRequest(this.eventBusFactory)(this.fakeGlobal, this.requestTracker, this.stubTracker, this.paramParser); 23 | }); 24 | 25 | it('extends from the global XMLHttpRequest', function() { 26 | var request = new this.FakeRequest(); 27 | 28 | expect(request.extraAttribute).toEqual('my cool attribute'); 29 | }); 30 | 31 | it('skips XMLHttpRequest attributes that IE does not want copied', function() { 32 | // use real window here so it will correctly go red on IE if it breaks 33 | var FakeRequest = getJasmineRequireObj().AjaxFakeRequest(this.eventBusFactory)(window, this.requestTracker, this.stubTracker, this.paramParser); 34 | var request = new FakeRequest(); 35 | 36 | expect(request.responseBody).toBeUndefined(); 37 | expect(request.responseXML).toBeUndefined(); 38 | expect(request.statusText).toBeUndefined(); 39 | }); 40 | 41 | it('tracks the request', function() { 42 | var request = new this.FakeRequest(); 43 | 44 | expect(this.requestTracker.track).toHaveBeenCalledWith(request); 45 | }); 46 | 47 | it('has default request headers and override mime type', function() { 48 | var request = new this.FakeRequest(); 49 | 50 | expect(request.requestHeaders).toEqual({}); 51 | expect(request.overriddenMimeType).toBeNull(); 52 | }); 53 | 54 | it('saves request information when opened', function() { 55 | var request = new this.FakeRequest(); 56 | request.open('METHOD', 'URL', 'ignore_async', 'USERNAME', 'PASSWORD'); 57 | 58 | expect(request.method).toEqual('METHOD'); 59 | expect(request.url).toEqual('URL'); 60 | expect(request.username).toEqual('USERNAME'); 61 | expect(request.password).toEqual('PASSWORD'); 62 | }); 63 | 64 | it('converts the url to a string', function() { 65 | var request = new this.FakeRequest(); 66 | request.open('METHOD', undefined); 67 | 68 | expect(request.method).toEqual('METHOD'); 69 | expect(request.url).toEqual('undefined'); 70 | }); 71 | 72 | it('saves an override mime type', function() { 73 | var request = new this.FakeRequest(); 74 | 75 | request.overrideMimeType('application/text; charset: utf-8'); 76 | 77 | expect(request.overriddenMimeType).toBe('application/text; charset: utf-8'); 78 | }); 79 | 80 | describe('when the request is not yet opened', function () { 81 | 82 | it('should throw an error', function () { 83 | var request = new this.FakeRequest(); 84 | 85 | expect(function () { 86 | request.setRequestHeader('X-Header-1', 'value1'); 87 | }).toThrowError('DOMException: Failed to execute "setRequestHeader" on "XMLHttpRequest": The object\'s state must be OPENED.'); 88 | }); 89 | }); 90 | 91 | describe('when the request is opened', function () { 92 | 93 | var request; 94 | 95 | beforeEach(function () { 96 | request = new this.FakeRequest(); 97 | 98 | request.open('METHOD', 'URL'); 99 | }); 100 | 101 | it('saves request headers', function() { 102 | request.setRequestHeader('X-Header-1', 'value1'); 103 | request.setRequestHeader('X-Header-2', 'value2'); 104 | 105 | expect(request.requestHeaders).toEqual({ 106 | 'X-Header-1': 'value1', 107 | 'X-Header-2': 'value2' 108 | }); 109 | }); 110 | 111 | it('combines request headers with the same header name', function() { 112 | request.setRequestHeader('X-Header', 'value1'); 113 | request.setRequestHeader('X-Header', 'value2'); 114 | 115 | expect(request.requestHeaders['X-Header']).toEqual('value1, value2'); 116 | }); 117 | 118 | it('finds the content-type request header', function() { 119 | request.setRequestHeader('ContEnt-tYPe', 'application/text+xml'); 120 | 121 | expect(request.contentType()).toEqual('application/text+xml'); 122 | }); 123 | 124 | it('clears the request headers when opened', function() { 125 | // Requirement #14 https://www.w3.org/TR/XMLHttpRequest/#the-open()-method 126 | request.setRequestHeader('X-Header1', 'value1'); 127 | 128 | expect(request.requestHeaders['X-Header1']).toEqual('value1'); 129 | 130 | request.open(); 131 | 132 | expect(request.requestHeaders['X-Header1']).not.toBeDefined(); 133 | expect(request.requestHeaders).toEqual({}); 134 | }); 135 | }); 136 | 137 | it('getResponseHeader returns null, if no response has been received', function() { 138 | var request = new this.FakeRequest(); 139 | expect(request.getResponseHeader('XY')).toBe(null); 140 | }); 141 | 142 | it('getAllResponseHeaders returns null, if no response has been received', function() { 143 | var request = new this.FakeRequest(); 144 | expect(request.getAllResponseHeaders()).toBe(null); 145 | }); 146 | 147 | describe('managing readyState', function() { 148 | beforeEach(function() { 149 | this.request = new this.FakeRequest(); 150 | }); 151 | 152 | it('has a static state UNSENT', function () { 153 | expect(this.FakeRequest.UNSENT).toBe(0); 154 | }); 155 | 156 | it('has a static state OPENED', function () { 157 | expect(this.FakeRequest.OPENED).toBe(1); 158 | }); 159 | 160 | it('has a static state HEADERS_RECEIVED', function () { 161 | expect(this.FakeRequest.HEADERS_RECEIVED).toBe(2); 162 | }); 163 | 164 | it('has a static state LOADING', function () { 165 | expect(this.FakeRequest.LOADING).toBe(3); 166 | }); 167 | 168 | it('has a static state DONE', function () { 169 | expect(this.FakeRequest.DONE).toBe(4); 170 | }); 171 | 172 | it('has an initial ready state of 0 (unsent)', function() { 173 | expect(this.request.readyState).toBe(0); 174 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalled(); 175 | }); 176 | 177 | it('has a ready state of 1 (opened) when opened', function() { 178 | this.request.open(); 179 | 180 | expect(this.request.readyState).toBe(1); 181 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('readystatechange'); 182 | }); 183 | 184 | it('has a ready state of 0 (unsent) when aborted', function() { 185 | this.request.open(); 186 | this.fakeEventBus.trigger.calls.reset(); 187 | 188 | this.request.abort(); 189 | 190 | expect(this.request.readyState).toBe(0); 191 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('readystatechange'); 192 | }); 193 | 194 | it('has a ready state of 1 (opened) when sent', function() { 195 | this.request.open(); 196 | this.fakeEventBus.trigger.calls.reset(); 197 | 198 | this.request.send(); 199 | 200 | expect(this.request.readyState).toBe(1); 201 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('loadstart'); 202 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('readystatechange'); 203 | }); 204 | 205 | it('has a ready state of 4 (done) when timed out', function() { 206 | this.request.open(); 207 | this.request.send(); 208 | this.fakeEventBus.trigger.calls.reset(); 209 | 210 | jasmine.clock().install(); 211 | this.request.responseTimeout(); 212 | jasmine.clock().uninstall(); 213 | 214 | expect(this.request.readyState).toBe(4); 215 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('readystatechange'); 216 | }); 217 | 218 | it('has a ready state of 4 (done) when network erroring', function() { 219 | this.request.open(); 220 | this.request.send(); 221 | this.fakeEventBus.trigger.calls.reset(); 222 | 223 | this.request.responseError(); 224 | 225 | expect(this.request.readyState).toBe(4); 226 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('readystatechange'); 227 | }); 228 | 229 | it('has a ready state of 4 (done) when responding', function() { 230 | this.request.open(); 231 | this.request.send(); 232 | this.fakeEventBus.trigger.calls.reset(); 233 | 234 | this.request.respondWith({}); 235 | 236 | expect(this.request.readyState).toBe(4); 237 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('readystatechange'); 238 | }); 239 | 240 | it('has a ready state of 2, then 4 (done) when responding', function() { 241 | this.request.open(); 242 | this.request.send(); 243 | this.fakeEventBus.trigger.calls.reset(); 244 | 245 | var request = this.request; 246 | var events = []; 247 | var headers = [ 248 | { name: 'X-Header', value: 'foo' } 249 | ]; 250 | 251 | this.fakeEventBus.trigger.and.callFake(function(event) { 252 | if (event === 'readystatechange') { 253 | events.push({ 254 | readyState: request.readyState, 255 | status: request.status, 256 | statusText: request.statusText, 257 | responseHeaders: request.responseHeaders 258 | }); 259 | } 260 | }); 261 | 262 | this.request.respondWith({ 263 | status: 200, 264 | statusText: 'OK', 265 | responseHeaders: headers 266 | }); 267 | 268 | expect(this.request.readyState).toBe(4); 269 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('readystatechange'); 270 | expect(events.length).toBe(2); 271 | expect(events).toEqual([ 272 | { readyState: 2, status: 200, statusText: 'OK', responseHeaders: headers }, 273 | { readyState: 4, status: 200, statusText: 'OK', responseHeaders: headers } 274 | ]); 275 | }); 276 | 277 | it('throws an error when timing out a request that has completed', function() { 278 | this.request.open(); 279 | this.request.send(); 280 | this.request.respondWith({}); 281 | var request = this.request; 282 | 283 | expect(function() { 284 | request.responseTimeout(); 285 | }).toThrowError('FakeXMLHttpRequest already completed'); 286 | }); 287 | 288 | it('throws an error when responding to a request that has completed', function() { 289 | this.request.open(); 290 | this.request.send(); 291 | this.request.respondWith({}); 292 | var request = this.request; 293 | 294 | expect(function() { 295 | request.respondWith({}); 296 | }).toThrowError('FakeXMLHttpRequest already completed'); 297 | }); 298 | 299 | it('throws an error when erroring a request that has completed', function() { 300 | this.request.open(); 301 | this.request.send(); 302 | this.request.respondWith({}); 303 | var request = this.request; 304 | 305 | expect(function() { 306 | request.responseError({}); 307 | }).toThrowError('FakeXMLHttpRequest already completed'); 308 | }); 309 | }); 310 | 311 | it('registers on-style callback with the event bus', function() { 312 | this.request = new this.FakeRequest(); 313 | 314 | expect(this.fakeEventBus.addEventListener).toHaveBeenCalledWith('readystatechange', jasmine.any(Function)); 315 | expect(this.fakeEventBus.addEventListener).toHaveBeenCalledWith('loadstart', jasmine.any(Function)); 316 | expect(this.fakeEventBus.addEventListener).toHaveBeenCalledWith('progress', jasmine.any(Function)); 317 | expect(this.fakeEventBus.addEventListener).toHaveBeenCalledWith('abort', jasmine.any(Function)); 318 | expect(this.fakeEventBus.addEventListener).toHaveBeenCalledWith('error', jasmine.any(Function)); 319 | expect(this.fakeEventBus.addEventListener).toHaveBeenCalledWith('load', jasmine.any(Function)); 320 | expect(this.fakeEventBus.addEventListener).toHaveBeenCalledWith('timeout', jasmine.any(Function)); 321 | expect(this.fakeEventBus.addEventListener).toHaveBeenCalledWith('loadend', jasmine.any(Function)); 322 | 323 | this.request.onreadystatechange = jasmine.createSpy('readystatechange'); 324 | this.request.onloadstart = jasmine.createSpy('loadstart'); 325 | this.request.onprogress = jasmine.createSpy('progress'); 326 | this.request.onabort = jasmine.createSpy('abort'); 327 | this.request.onerror = jasmine.createSpy('error'); 328 | this.request.onload = jasmine.createSpy('load'); 329 | this.request.ontimeout = jasmine.createSpy('timeout'); 330 | this.request.onloadend = jasmine.createSpy('loadend'); 331 | 332 | var args = this.fakeEventBus.addEventListener.calls.allArgs(); 333 | for (var i = 0; i < args.length; i++) { 334 | var eventName = args[i][0], 335 | busCallback = args[i][1]; 336 | 337 | busCallback(); 338 | expect(this.request['on' + eventName]).toHaveBeenCalled(); 339 | } 340 | }); 341 | 342 | it('delegates addEventListener to the eventBus', function() { 343 | this.request = new this.FakeRequest(); 344 | 345 | this.request.addEventListener('foo', 'bar'); 346 | 347 | expect(this.fakeEventBus.addEventListener).toHaveBeenCalledWith('foo', 'bar'); 348 | }); 349 | 350 | it('delegates removeEventListener to the eventBus', function() { 351 | this.request = new this.FakeRequest(); 352 | 353 | this.request.removeEventListener('foo', 'bar'); 354 | 355 | expect(this.fakeEventBus.removeEventListener).toHaveBeenCalledWith('foo', 'bar'); 356 | }); 357 | 358 | describe('triggering progress events', function() { 359 | beforeEach(function() { 360 | this.request = new this.FakeRequest(); 361 | }); 362 | 363 | it('should not trigger any events to start', function() { 364 | this.request.open(); 365 | 366 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('readystatechange'); 367 | }); 368 | 369 | it('should trigger loadstart when sent', function() { 370 | this.request.open(); 371 | 372 | this.fakeEventBus.trigger.calls.reset(); 373 | 374 | this.request.send(); 375 | 376 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('loadstart'); 377 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('readystatechange'); 378 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('progress'); 379 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('abort'); 380 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('error'); 381 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('load'); 382 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('timeout'); 383 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('loadend'); 384 | }); 385 | 386 | it('should trigger abort, progress, loadend when aborted', function() { 387 | this.request.open(); 388 | this.request.send(); 389 | 390 | this.fakeEventBus.trigger.calls.reset(); 391 | 392 | this.request.abort(); 393 | 394 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('readystatechange'); 395 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('loadstart'); 396 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('progress'); 397 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('abort'); 398 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('error'); 399 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('load'); 400 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('timeout'); 401 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('loadend'); 402 | }); 403 | 404 | it('should trigger error, progress, loadend when network error', function() { 405 | this.request.open(); 406 | this.request.send(); 407 | 408 | this.fakeEventBus.trigger.calls.reset(); 409 | 410 | this.request.responseError(); 411 | 412 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('loadstart'); 413 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('readystatechange'); 414 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('progress'); 415 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('abort'); 416 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('error'); 417 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('load'); 418 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('timeout'); 419 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('loadend'); 420 | }); 421 | 422 | it('should trigger timeout, progress, loadend when timing out', function() { 423 | this.request.open(); 424 | this.request.send(); 425 | 426 | this.fakeEventBus.trigger.calls.reset(); 427 | 428 | jasmine.clock().install(); 429 | this.request.responseTimeout(); 430 | jasmine.clock().uninstall(); 431 | 432 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('loadstart'); 433 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('readystatechange'); 434 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('progress'); 435 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('abort'); 436 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('error'); 437 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('load'); 438 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('timeout'); 439 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('loadend'); 440 | }); 441 | 442 | it('should trigger load, progress, loadend when responding', function() { 443 | this.request.open(); 444 | this.request.send(); 445 | 446 | this.fakeEventBus.trigger.calls.reset(); 447 | 448 | this.request.respondWith({ status: 200 }); 449 | 450 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('loadstart'); 451 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('readystatechange'); 452 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('progress'); 453 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('abort'); 454 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('error'); 455 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('load'); 456 | expect(this.fakeEventBus.trigger).not.toHaveBeenCalledWith('timeout'); 457 | expect(this.fakeEventBus.trigger).toHaveBeenCalledWith('loadend'); 458 | }); 459 | }); 460 | 461 | it('ticks the jasmine clock on timeout', function() { 462 | var clock = { tick: jasmine.createSpy('tick') }; 463 | spyOn(jasmine, 'clock').and.returnValue(clock); 464 | 465 | var request = new this.FakeRequest(); 466 | request.open(); 467 | request.send(); 468 | 469 | request.responseTimeout(); 470 | 471 | expect(clock.tick).toHaveBeenCalledWith(30000); 472 | }); 473 | 474 | it('has an initial status of null', function() { 475 | var request = new this.FakeRequest(); 476 | 477 | expect(request.status).toBeNull(); 478 | }); 479 | 480 | it('has an aborted status', function() { 481 | var request = new this.FakeRequest(); 482 | 483 | request.abort(); 484 | 485 | expect(request.status).toBe(0); 486 | expect(request.statusText).toBe('abort'); 487 | }); 488 | 489 | it('has a status from the response', function() { 490 | var request = new this.FakeRequest(); 491 | request.open(); 492 | request.send(); 493 | 494 | request.respondWith({ status: 200 }); 495 | 496 | expect(request.status).toBe(200); 497 | expect(request.statusText).toBe(''); 498 | }); 499 | 500 | it('has a statusText from the response', function() { 501 | var request = new this.FakeRequest(); 502 | request.open(); 503 | request.send(); 504 | 505 | request.respondWith({ status: 200, statusText: 'OK' }); 506 | 507 | expect(request.status).toBe(200); 508 | expect(request.statusText).toBe('OK'); 509 | }); 510 | 511 | it('has a status from the response when there is an error', function() { 512 | var request = new this.FakeRequest(); 513 | request.open(); 514 | request.send(); 515 | 516 | request.responseError({ status: 500 }); 517 | 518 | expect(request.status).toBe(500); 519 | expect(request.statusText).toBe(''); 520 | }); 521 | 522 | it('has a statusText from the response when there is an error', function() { 523 | var request = new this.FakeRequest(); 524 | request.open(); 525 | request.send(); 526 | 527 | request.responseError({ status: 500, statusText: 'Internal Server Error' }); 528 | 529 | expect(request.status).toBe(500); 530 | expect(request.statusText).toBe('Internal Server Error'); 531 | }); 532 | 533 | 534 | it('saves off any data sent to the server', function() { 535 | var request = new this.FakeRequest(); 536 | request.open(); 537 | request.send('foo=bar&baz=quux'); 538 | 539 | expect(request.params).toBe('foo=bar&baz=quux'); 540 | }); 541 | 542 | it('parses data sent to the server', function() { 543 | var request = new this.FakeRequest(); 544 | request.open(); 545 | request.send('foo=bar&baz=quux'); 546 | 547 | this.parserInstance.and.returnValue('parsed'); 548 | 549 | expect(request.data()).toBe('parsed'); 550 | }); 551 | 552 | it('skips parsing if no data was sent', function() { 553 | var request = new this.FakeRequest(); 554 | request.open(); 555 | request.send(); 556 | 557 | expect(request.data()).toEqual({}); 558 | expect(this.parserInstance).not.toHaveBeenCalled(); 559 | }); 560 | 561 | it('saves responseText', function() { 562 | var request = new this.FakeRequest(); 563 | request.open(); 564 | request.send(); 565 | 566 | request.respondWith({ status: 200, responseText: 'foobar' }); 567 | 568 | expect(request.responseText).toBe('foobar'); 569 | }); 570 | 571 | it('defaults responseText if none is given', function() { 572 | var request = new this.FakeRequest(); 573 | request.open(); 574 | request.send(); 575 | 576 | request.respondWith({ status: 200 }); 577 | 578 | expect(request.responseText).toBe(''); 579 | }); 580 | 581 | it('saves responseURL', function() { 582 | var request = new this.FakeRequest(); 583 | request.open(); 584 | request.send(); 585 | 586 | request.respondWith({ status: 200, responseText: 'foobar', responseURL: 'foo.bar/redirect' }); 587 | 588 | expect(request.responseURL).toBe('foo.bar/redirect'); 589 | }); 590 | 591 | it('defaults responseURL if none is given', function() { 592 | var request = new this.FakeRequest(); 593 | request.open(); 594 | request.send(); 595 | 596 | request.respondWith({ status: 200 }); 597 | 598 | expect(request.responseURL).toBe(null); 599 | }); 600 | 601 | it('retrieves individual response headers', function() { 602 | var request = new this.FakeRequest(); 603 | request.open(); 604 | request.send(); 605 | 606 | request.respondWith({ 607 | status: 200, 608 | responseHeaders: { 609 | 'X-Header': 'foo' 610 | } 611 | }); 612 | 613 | expect(request.getResponseHeader('X-Header')).toBe('foo'); 614 | }); 615 | 616 | it('retrieves individual response headers case-insensitively', function() { 617 | var request = new this.FakeRequest(); 618 | request.open(); 619 | request.send(); 620 | 621 | request.respondWith({ 622 | status: 200, 623 | responseHeaders: { 624 | 'X-Header': 'foo' 625 | } 626 | }); 627 | 628 | expect(request.getResponseHeader('x-header')).toBe('foo'); 629 | }); 630 | 631 | it('retrieves a combined response header', function() { 632 | var request = new this.FakeRequest(); 633 | request.open(); 634 | request.send(); 635 | 636 | request.respondWith({ 637 | status: 200, 638 | responseHeaders: [ 639 | { name: 'X-Header', value: 'foo' }, 640 | { name: 'X-Header', value: 'bar' } 641 | ] 642 | }); 643 | 644 | expect(request.getResponseHeader('x-header')).toBe('foo, bar'); 645 | }); 646 | 647 | it("doesn't pollute the response headers of other XHRs", function() { 648 | var request1 = new this.FakeRequest(); 649 | request1.open(); 650 | request1.send(); 651 | 652 | var request2 = new this.FakeRequest(); 653 | request2.open(); 654 | request2.send(); 655 | 656 | request1.respondWith({ status: 200, responseHeaders: { 'X-Foo': 'bar' } }); 657 | request2.respondWith({ status: 200, responseHeaders: { 'X-Baz': 'quux' } }); 658 | 659 | expect(request1.getAllResponseHeaders()).toBe("X-Foo: bar\r\n"); 660 | expect(request2.getAllResponseHeaders()).toBe("X-Baz: quux\r\n"); 661 | }); 662 | 663 | it('retrieves all response headers', function() { 664 | var request = new this.FakeRequest(); 665 | request.open(); 666 | request.send(); 667 | 668 | request.respondWith({ 669 | status: 200, 670 | responseHeaders: [ 671 | { name: 'X-Header-1', value: 'foo' }, 672 | { name: 'X-Header-2', value: 'bar' }, 673 | { name: 'X-Header-1', value: 'baz' } 674 | ] 675 | }); 676 | 677 | expect(request.getAllResponseHeaders()).toBe("X-Header-1: foo\r\nX-Header-2: bar\r\nX-Header-1: baz\r\n"); 678 | }); 679 | 680 | it('sets the content-type header to the specified contentType when no other headers are supplied', function() { 681 | var request = new this.FakeRequest(); 682 | request.open(); 683 | request.send(); 684 | 685 | request.respondWith({ status: 200, contentType: 'text/plain' }); 686 | 687 | expect(request.getResponseHeader('content-type')).toBe('text/plain'); 688 | expect(request.getAllResponseHeaders()).toBe("Content-Type: text/plain\r\n"); 689 | }); 690 | 691 | it('sets a default content-type header if no contentType and headers are supplied', function() { 692 | var request = new this.FakeRequest(); 693 | request.open(); 694 | request.send(); 695 | 696 | request.respondWith({ status: 200 }); 697 | 698 | expect(request.getResponseHeader('content-type')).toBe('application/json'); 699 | expect(request.getAllResponseHeaders()).toBe("Content-Type: application/json\r\n"); 700 | }); 701 | 702 | it('has no responseXML by default', function() { 703 | var request = new this.FakeRequest(); 704 | request.open(); 705 | request.send(); 706 | 707 | request.respondWith({ status: 200 }); 708 | 709 | expect(request.responseXML).toBeNull(); 710 | }); 711 | 712 | it('parses a text/xml document into responseXML', function() { 713 | var request = new this.FakeRequest(); 714 | request.open(); 715 | request.send(); 716 | 717 | request.respondWith({ status: 200, contentType: 'text/xml', responseText: '' }); 718 | 719 | if (typeof window.Document !== 'undefined') { 720 | expect(request.responseXML instanceof window.Document).toBe(true); 721 | expect(request.response instanceof window.Document).toBe(true); 722 | } else { 723 | // IE 8 724 | expect(request.responseXML instanceof window.ActiveXObject).toBe(true); 725 | expect(request.response instanceof window.ActiveXObject).toBe(true); 726 | } 727 | }); 728 | 729 | it('parses an application/xml document into responseXML', function() { 730 | var request = new this.FakeRequest(); 731 | request.open(); 732 | request.send(); 733 | 734 | request.respondWith({ status: 200, contentType: 'application/xml', responseText: '' }); 735 | 736 | if (typeof window.Document !== 'undefined') { 737 | expect(request.responseXML instanceof window.Document).toBe(true); 738 | expect(request.response instanceof window.Document).toBe(true); 739 | } else { 740 | // IE 8 741 | expect(request.responseXML instanceof window.ActiveXObject).toBe(true); 742 | expect(request.response instanceof window.ActiveXObject).toBe(true); 743 | } 744 | }); 745 | 746 | it('parses a custom blah+xml document into responseXML', function() { 747 | var request = new this.FakeRequest(); 748 | request.open(); 749 | request.send(); 750 | 751 | request.respondWith({ status: 200, contentType: 'application/text+xml', responseText: '' }); 752 | 753 | if (typeof window.Document !== 'undefined') { 754 | expect(request.responseXML instanceof window.Document).toBe(true); 755 | expect(request.response instanceof window.Document).toBe(true); 756 | } else { 757 | // IE 8 758 | expect(request.responseXML instanceof window.ActiveXObject).toBe(true); 759 | expect(request.response instanceof window.ActiveXObject).toBe(true); 760 | } 761 | }); 762 | 763 | it('stringifies responseJSON into responseText', function() { 764 | var request = new this.FakeRequest(); 765 | request.open(); 766 | request.send(); 767 | 768 | request.respondWith({ status: 200, responseJSON: {'foo': 'bar'} }); 769 | 770 | expect(request.response).toEqual('{"foo":"bar"}'); 771 | }); 772 | 773 | it('defaults the response attribute to the responseText', function() { 774 | var request = new this.FakeRequest(); 775 | request.open(); 776 | request.send(); 777 | 778 | request.respondWith({ status: 200, responseText: 'foo' }); 779 | 780 | expect(request.response).toEqual('foo'); 781 | }); 782 | 783 | it('has a text response when the responseType is blank', function() { 784 | var request = new this.FakeRequest(); 785 | request.open(); 786 | request.send(); 787 | 788 | request.respondWith({ status: 200, responseText: 'foo', responseType: '' }); 789 | 790 | expect(request.response).toEqual('foo'); 791 | }); 792 | 793 | it('has a text response when the responseType is text', function() { 794 | var request = new this.FakeRequest(); 795 | request.open(); 796 | request.send(); 797 | 798 | request.respondWith({ status: 200, responseText: 'foo', responseType: 'text' }); 799 | 800 | expect(request.response).toEqual('foo'); 801 | }); 802 | 803 | describe('stream response', function() { 804 | it('can return a response', function() { 805 | var request = new this.FakeRequest(); 806 | request.open(); 807 | request.send(); 808 | 809 | request.startStream(); 810 | 811 | expect(request.readyState).toBe(this.FakeRequest.LOADING); 812 | expect(request.responseText).toBe(''); 813 | }); 814 | 815 | it('can finish request', function() { 816 | var request = new this.FakeRequest(); 817 | request.open(); 818 | request.send(); 819 | 820 | request.startStream(); 821 | request.completeStream(); 822 | 823 | expect(request.readyState).toBe(this.FakeRequest.DONE); 824 | expect(request.responseText).toBe(''); 825 | expect(request.status).toBe(200); 826 | }); 827 | 828 | it('can cancel request', function() { 829 | var request = new this.FakeRequest(); 830 | request.open(); 831 | request.send(); 832 | 833 | request.startStream(); 834 | expect(request.status).toBe(200); 835 | 836 | request.cancelStream(); 837 | 838 | expect(request.readyState).toBe(this.FakeRequest.DONE); 839 | expect(request.responseText).toBe(''); 840 | expect(request.status).toBe(0); 841 | }); 842 | 843 | it('can send part of data as response', function() { 844 | var request = new this.FakeRequest(); 845 | request.open(); 846 | request.send(); 847 | 848 | request.startStream(); 849 | request.streamData('text\n'); 850 | 851 | expect(request.readyState).toBe(this.FakeRequest.LOADING); 852 | expect(request.responseText).toBe('text\n'); 853 | 854 | request.streamData('text2\n'); 855 | 856 | expect(request.responseText).toBe('text\ntext2\n'); 857 | 858 | request.completeStream(); 859 | }); 860 | 861 | it('thrown an error if finish request and then try to cancel', function() { 862 | var request = new this.FakeRequest(); 863 | request.open(); 864 | request.send(); 865 | 866 | request.startStream(); 867 | request.completeStream(); 868 | 869 | expect(function() { 870 | request.cancelStream(); 871 | }).toThrowError(); 872 | }); 873 | }); 874 | }); 875 | -------------------------------------------------------------------------------- /spec/helpers/spec-helper.js: -------------------------------------------------------------------------------- 1 | // beforeEach(function() { 2 | // clearAjaxRequests(); 3 | // }); 4 | -------------------------------------------------------------------------------- /spec/integration/mock-ajax-spec.js: -------------------------------------------------------------------------------- 1 | describe("mockAjax", function() { 2 | it("throws an error if global XMLHttpRequest is no longer the original", function() { 3 | var fakeXmlHttpRequest = jasmine.createSpy('fakeXmlHttpRequest'), 4 | fakeGlobal = { XMLHttpRequest: fakeXmlHttpRequest }, 5 | mockAjax = new window.MockAjax(fakeGlobal); 6 | 7 | fakeGlobal.XMLHttpRequest = function() {}; 8 | 9 | expect(function() { 10 | mockAjax.install(); 11 | }).toThrowError(); 12 | }); 13 | 14 | it("does not throw an error if uninstalled between installs", function() { 15 | var fakeXmlHttpRequest = jasmine.createSpy('fakeXmlHttpRequest'), 16 | fakeGlobal = { XMLHttpRequest: fakeXmlHttpRequest }, 17 | mockAjax = new window.MockAjax(fakeGlobal); 18 | 19 | function sequentialInstalls() { 20 | mockAjax.install(); 21 | mockAjax.uninstall(); 22 | mockAjax.install(); 23 | } 24 | 25 | expect(sequentialInstalls).not.toThrow(); 26 | }); 27 | 28 | it("does throw an error if uninstalled without a current install", function() { 29 | var fakeXmlHttpRequest = jasmine.createSpy('fakeXmlHttpRequest'), 30 | fakeGlobal = { XMLHttpRequest: fakeXmlHttpRequest }, 31 | mockAjax = new window.MockAjax(fakeGlobal); 32 | 33 | expect(function() { 34 | mockAjax.uninstall(); 35 | }).toThrowError(); 36 | }); 37 | 38 | it("does not replace XMLHttpRequest until it is installed", function() { 39 | var fakeXmlHttpRequest = jasmine.createSpy('fakeXmlHttpRequest'), 40 | fakeGlobal = { XMLHttpRequest: fakeXmlHttpRequest }, 41 | mockAjax = new window.MockAjax(fakeGlobal); 42 | 43 | fakeGlobal.XMLHttpRequest('foo'); 44 | expect(fakeXmlHttpRequest).toHaveBeenCalledWith('foo'); 45 | fakeXmlHttpRequest.calls.reset(); 46 | 47 | mockAjax.install(); 48 | fakeGlobal.XMLHttpRequest('foo'); 49 | expect(fakeXmlHttpRequest).not.toHaveBeenCalled(); 50 | }); 51 | 52 | it("replaces the global XMLHttpRequest on uninstall", function() { 53 | var fakeXmlHttpRequest = jasmine.createSpy('fakeXmlHttpRequest'), 54 | fakeGlobal = { XMLHttpRequest: fakeXmlHttpRequest }, 55 | mockAjax = new window.MockAjax(fakeGlobal); 56 | 57 | mockAjax.install(); 58 | mockAjax.uninstall(); 59 | 60 | fakeGlobal.XMLHttpRequest('foo'); 61 | expect(fakeXmlHttpRequest).toHaveBeenCalledWith('foo'); 62 | }); 63 | 64 | it("clears requests and stubs upon uninstall", function() { 65 | var fakeXmlHttpRequest = jasmine.createSpy('fakeXmlHttpRequest'), 66 | fakeGlobal = { XMLHttpRequest: fakeXmlHttpRequest }, 67 | mockAjax = new window.MockAjax(fakeGlobal); 68 | 69 | mockAjax.install(); 70 | 71 | mockAjax.requests.track({url: '/testurl'}); 72 | mockAjax.stubRequest('/bobcat'); 73 | 74 | expect(mockAjax.requests.count()).toEqual(1); 75 | expect(mockAjax.stubs.findStub('/bobcat')).toBeDefined(); 76 | 77 | mockAjax.uninstall(); 78 | 79 | expect(mockAjax.requests.count()).toEqual(0); 80 | expect(mockAjax.stubs.findStub('/bobcat')).not.toBeDefined(); 81 | }); 82 | 83 | it("allows the httpRequest to be retrieved", function() { 84 | var fakeXmlHttpRequest = jasmine.createSpy('fakeXmlHttpRequest'), 85 | fakeGlobal = { XMLHttpRequest: fakeXmlHttpRequest }, 86 | mockAjax = new window.MockAjax(fakeGlobal); 87 | 88 | mockAjax.install(); 89 | var request = new fakeGlobal.XMLHttpRequest(); 90 | 91 | expect(mockAjax.requests.count()).toBe(1); 92 | expect(mockAjax.requests.mostRecent()).toBe(request); 93 | }); 94 | 95 | it("allows the httpRequests to be cleared", function() { 96 | var fakeXmlHttpRequest = jasmine.createSpy('fakeXmlHttpRequest'), 97 | fakeGlobal = { XMLHttpRequest: fakeXmlHttpRequest }, 98 | mockAjax = new window.MockAjax(fakeGlobal); 99 | 100 | mockAjax.install(); 101 | var request = new fakeGlobal.XMLHttpRequest(); 102 | 103 | expect(mockAjax.requests.mostRecent()).toBe(request); 104 | mockAjax.requests.reset(); 105 | expect(mockAjax.requests.count()).toBe(0); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /spec/integration/webmock-style-spec.js: -------------------------------------------------------------------------------- 1 | describe("Webmock style mocking", function() { 2 | var successSpy, errorSpy, timeoutSpy, response, fakeGlobal, mockAjax; 3 | 4 | var sendRequest = function(fakeGlobal, url, method) { 5 | url = url || "http://example.com/someApi"; 6 | method = method || 'GET'; 7 | var xhr = new fakeGlobal.XMLHttpRequest(); 8 | xhr.onreadystatechange = function(args) { 9 | if (this.readyState === (this.DONE || 4)) { // IE 8 doesn't support DONE 10 | response = this; 11 | successSpy(); 12 | } 13 | }; 14 | 15 | xhr.onerror = function() { 16 | errorSpy(); 17 | }; 18 | 19 | xhr.ontimeout = function() { 20 | timeoutSpy(); 21 | }; 22 | 23 | xhr.open(method, url); 24 | xhr.send(); 25 | }; 26 | 27 | beforeEach(function() { 28 | successSpy = jasmine.createSpy('success'); 29 | errorSpy = jasmine.createSpy('error'); 30 | timeoutSpy = jasmine.createSpy('timeout'); 31 | fakeGlobal = {XMLHttpRequest: jasmine.createSpy('realXMLHttpRequest')}; 32 | mockAjax = new window.MockAjax(fakeGlobal); 33 | mockAjax.install(); 34 | 35 | mockAjax.stubRequest("http://example.com/someApi").andReturn({responseText: "hi!"}); 36 | mockAjax.stubRequest("http://example.com/someErrorApi").andError(); 37 | mockAjax.stubRequest("http://example.com/someTimeoutApi").andTimeout(); 38 | }); 39 | 40 | it("allows a url to be setup as a stub", function() { 41 | sendRequest(fakeGlobal); 42 | expect(successSpy).toHaveBeenCalled(); 43 | }); 44 | 45 | it("allows a url to be setup as a stub which trigger error", function() { 46 | sendRequest(fakeGlobal, "http://example.com/someErrorApi"); 47 | expect(errorSpy).toHaveBeenCalled(); 48 | }); 49 | 50 | it("allows a url to be setup as a stub which timeouts", function() { 51 | jasmine.clock().install(); 52 | sendRequest(fakeGlobal, "http://example.com/someTimeoutApi"); 53 | expect(timeoutSpy).toHaveBeenCalled(); 54 | jasmine.clock().uninstall(); 55 | }); 56 | 57 | it("should allow you to clear all the ajax stubs", function() { 58 | mockAjax.stubs.reset(); 59 | sendRequest(fakeGlobal); 60 | expect(successSpy).not.toHaveBeenCalled(); 61 | }); 62 | 63 | it("should set the contentType", function() { 64 | sendRequest(fakeGlobal); 65 | expect(response.getResponseHeader('Content-Type')).toEqual('application/json'); 66 | }); 67 | 68 | it("should set the responseText", function() { 69 | sendRequest(fakeGlobal); 70 | expect(response.responseText).toEqual('hi!'); 71 | }); 72 | 73 | it("should default the status to 200", function() { 74 | sendRequest(fakeGlobal); 75 | expect(response.status).toEqual(200); 76 | }); 77 | 78 | it("should set the responseHeaders", function() { 79 | mockAjax.stubRequest("http://example.com/someApi").andReturn({ 80 | responseText: "hi!", 81 | responseHeaders: [{name: "X-Custom", value: "header value"}] 82 | }); 83 | sendRequest(fakeGlobal); 84 | expect(response.getResponseHeader('X-Custom')).toEqual('header value'); 85 | }); 86 | 87 | it("shoiuld set the status info", function() { 88 | mockAjax.stubRequest("http://example.com/someApi").andReturn({ 89 | status: 201, 90 | statusText: 'HTTP/1.1 201 CREATED' 91 | }); 92 | sendRequest(fakeGlobal); 93 | expect(response.status).toEqual(201); 94 | expect(response.statusText).toEqual('HTTP/1.1 201 CREATED'); 95 | }); 96 | 97 | describe("with another stub for the same url", function() { 98 | beforeEach(function() { 99 | mockAjax.stubRequest("http://example.com/someApi").andReturn({responseText: "no", status: 403}); 100 | sendRequest(fakeGlobal); 101 | }); 102 | 103 | it("should set the status", function() { 104 | expect(response.status).toEqual(403); 105 | }); 106 | 107 | it("should allow the latest stub to win", function() { 108 | expect(response.responseText).toEqual('no'); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /spec/integration/with-mock-spec.js: -------------------------------------------------------------------------------- 1 | describe("withMock", function() { 2 | var sendRequest = function(fakeGlobal) { 3 | var xhr = new fakeGlobal.XMLHttpRequest(); 4 | 5 | xhr.open("GET", "http://example.com/someApi"); 6 | xhr.send(); 7 | }; 8 | 9 | it("installs the mock for passed in function, and uninstalls when complete", function() { 10 | var xmlHttpRequest = jasmine.createSpyObj('XMLHttpRequest', ['open', 'send']), 11 | xmlHttpRequestCtor = spyOn(window, 'XMLHttpRequest').and.returnValue(xmlHttpRequest), 12 | fakeGlobal = {XMLHttpRequest: xmlHttpRequestCtor}, 13 | mockAjax = new window.MockAjax(fakeGlobal); 14 | 15 | mockAjax.withMock(function() { 16 | sendRequest(fakeGlobal); 17 | expect(xmlHttpRequest.open).not.toHaveBeenCalled(); 18 | }); 19 | 20 | sendRequest(fakeGlobal); 21 | expect(xmlHttpRequest.open).toHaveBeenCalled(); 22 | }); 23 | 24 | it("properly uninstalls when the passed in function throws", function() { 25 | var xmlHttpRequest = jasmine.createSpyObj('XMLHttpRequest', ['open', 'send']), 26 | xmlHttpRequestCtor = spyOn(window, 'XMLHttpRequest').and.returnValue(xmlHttpRequest), 27 | fakeGlobal = {XMLHttpRequest: xmlHttpRequestCtor}, 28 | mockAjax = new window.MockAjax(fakeGlobal); 29 | 30 | expect(function() { 31 | mockAjax.withMock(function() { 32 | throw "error"; 33 | }); 34 | }).toThrow("error"); 35 | 36 | sendRequest(fakeGlobal); 37 | expect(xmlHttpRequest.open).toHaveBeenCalled(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /spec/mock-ajax-toplevel-spec.js: -------------------------------------------------------------------------------- 1 | /*global sharedAjaxResponseBehaviorForZepto_Failure: true, sharedAjaxResponseBehaviorForZepto_Success: true */ 2 | // jshint latedef: nofunc 3 | 4 | describe("Jasmine Mock Ajax (for toplevel)", function() { 5 | var request, anotherRequest, response; 6 | var success, error, complete; 7 | var client, onreadystatechange; 8 | var sharedContext = {}; 9 | var fakeGlobal, mockAjax; 10 | 11 | beforeEach(function() { 12 | var fakeXMLHttpRequest = jasmine.createSpy('realFakeXMLHttpRequest'); 13 | fakeGlobal = { 14 | XMLHttpRequest: fakeXMLHttpRequest, 15 | DOMParser: window.DOMParser, 16 | ActiveXObject: window.ActiveXObject 17 | }; 18 | mockAjax = new window.MockAjax(fakeGlobal); 19 | mockAjax.install(); 20 | 21 | success = jasmine.createSpy("onSuccess"); 22 | error = jasmine.createSpy("onFailure"); 23 | complete = jasmine.createSpy("onComplete"); 24 | 25 | onreadystatechange = function() { 26 | if (this.readyState === (this.DONE || 4)) { // IE 8 doesn't support DONE 27 | if (this.status === 200) { 28 | success(this.responseText, this.textStatus, this); 29 | } else { 30 | error(this, this.textStatus, ''); 31 | } 32 | 33 | complete(this, this.textStatus); 34 | } 35 | }; 36 | }); 37 | 38 | describe("when making a request", function () { 39 | beforeEach(function() { 40 | client = new fakeGlobal.XMLHttpRequest(); 41 | client.onreadystatechange = onreadystatechange; 42 | client.open("GET", "example.com/someApi"); 43 | client.send(); 44 | request = mockAjax.requests.mostRecent(); 45 | }); 46 | 47 | it("should store URL and transport", function() { 48 | expect(request.url).toEqual("example.com/someApi"); 49 | }); 50 | 51 | it("should queue the request", function() { 52 | expect(mockAjax.requests.count()).toEqual(1); 53 | }); 54 | 55 | it("should allow access to the queued request", function() { 56 | expect(mockAjax.requests.first()).toEqual(request); 57 | }); 58 | 59 | it("should allow access to the queued request via index", function() { 60 | expect(mockAjax.requests.at(0)).toEqual(request); 61 | }); 62 | 63 | describe("and then another request", function () { 64 | beforeEach(function() { 65 | client = new fakeGlobal.XMLHttpRequest(); 66 | client.onreadystatechange = onreadystatechange; 67 | client.open("GET", "example.com/someApi"); 68 | client.send(); 69 | 70 | anotherRequest = mockAjax.requests.mostRecent(); 71 | }); 72 | 73 | it("should queue the next request", function() { 74 | expect(mockAjax.requests.count()).toEqual(2); 75 | }); 76 | 77 | it("should allow access to the other queued request", function() { 78 | expect(mockAjax.requests.first()).toEqual(request); 79 | expect(mockAjax.requests.mostRecent()).toEqual(anotherRequest); 80 | }); 81 | }); 82 | 83 | describe("mockAjax.requests.mostRecent()", function () { 84 | 85 | describe("when there is one request queued", function () { 86 | it("should return the request", function() { 87 | expect(mockAjax.requests.mostRecent()).toEqual(request); 88 | }); 89 | }); 90 | 91 | describe("when there is more than one request", function () { 92 | beforeEach(function() { 93 | client = new fakeGlobal.XMLHttpRequest(); 94 | client.onreadystatechange = onreadystatechange; 95 | client.open("GET", "example.com/someApi"); 96 | client.send(); 97 | anotherRequest = mockAjax.requests.mostRecent(); 98 | }); 99 | 100 | it("should return the most recent request", function() { 101 | expect(mockAjax.requests.mostRecent()).toEqual(anotherRequest); 102 | }); 103 | }); 104 | 105 | describe("when there are no requests", function () { 106 | beforeEach(function() { 107 | mockAjax.requests.reset(); 108 | }); 109 | 110 | it("should return null", function() { 111 | expect(mockAjax.requests.mostRecent()).toBeUndefined(); 112 | }); 113 | }); 114 | }); 115 | 116 | describe("clearAjaxRequests()", function () { 117 | beforeEach(function() { 118 | mockAjax.requests.reset(); 119 | }); 120 | 121 | it("should remove all requests", function() { 122 | expect(mockAjax.requests.count()).toEqual(0); 123 | expect(mockAjax.requests.mostRecent()).toBeUndefined(); 124 | }); 125 | }); 126 | }); 127 | 128 | describe("when simulating a response with request.response", function () { 129 | describe("and the response is Success", function () { 130 | beforeEach(function() { 131 | client = new fakeGlobal.XMLHttpRequest(); 132 | client.onreadystatechange = onreadystatechange; 133 | client.open("GET", "example.com/someApi"); 134 | client.setRequestHeader("Content-Type", "text/plain"); 135 | client.send(); 136 | 137 | request = mockAjax.requests.mostRecent(); 138 | response = {status: 200, statusText: "OK", contentType: "text/html", responseText: "OK!"}; 139 | request.respondWith(response); 140 | 141 | sharedContext.responseCallback = success; 142 | sharedContext.status = response.status; 143 | sharedContext.statusText = response.statusText; 144 | sharedContext.contentType = response.contentType; 145 | sharedContext.responseText = response.responseText; 146 | sharedContext.responseType = response.responseType; 147 | sharedContext.responseURL = response.responseURL; 148 | }); 149 | 150 | it("should call the success handler", function() { 151 | expect(success).toHaveBeenCalled(); 152 | }); 153 | 154 | it("should not call the failure handler", function() { 155 | expect(error).not.toHaveBeenCalled(); 156 | }); 157 | 158 | it("should call the complete handler", function() { 159 | expect(complete).toHaveBeenCalled(); 160 | }); 161 | 162 | sharedAjaxResponseBehaviorForZepto_Success(sharedContext); 163 | }); 164 | 165 | describe("and the response is Success, but with JSON", function () { 166 | beforeEach(function() { 167 | client = new fakeGlobal.XMLHttpRequest(); 168 | client.onreadystatechange = onreadystatechange; 169 | client.open("GET", "example.com/someApi"); 170 | client.setRequestHeader("Content-Type", "application/json"); 171 | client.send(); 172 | 173 | request = mockAjax.requests.mostRecent(); 174 | var responseObject = {status: 200, statusText: "OK", contentType: "application/json", responseText: '{"foo":"bar"}', responseType: "json"}; 175 | 176 | request.respondWith(responseObject); 177 | 178 | sharedContext.responseCallback = success; 179 | sharedContext.status = responseObject.status; 180 | sharedContext.statusText = responseObject.statusText; 181 | sharedContext.contentType = responseObject.contentType; 182 | sharedContext.responseText = responseObject.responseText; 183 | sharedContext.responseType = responseObject.responseType; 184 | sharedContext.responseURL = responseObject.responseURL; 185 | 186 | response = success.calls.mostRecent().args[2]; 187 | }); 188 | 189 | it("should call the success handler", function() { 190 | expect(success).toHaveBeenCalled(); 191 | }); 192 | 193 | it("should not call the failure handler", function() { 194 | expect(error).not.toHaveBeenCalled(); 195 | }); 196 | 197 | it("should call the complete handler", function() { 198 | expect(complete).toHaveBeenCalled(); 199 | }); 200 | 201 | it("should return a JavaScript object for XHR2 response", function() { 202 | var responseText = sharedContext.responseText; 203 | expect(success.calls.mostRecent().args[0]).toEqual(responseText); 204 | 205 | expect(response.responseText).toEqual(responseText); 206 | expect(response.response).toEqual({foo: "bar"}); 207 | }); 208 | 209 | sharedAjaxResponseBehaviorForZepto_Success(sharedContext); 210 | }); 211 | 212 | describe("and the response is Success, and response is overriden", function () { 213 | beforeEach(function() { 214 | client = new fakeGlobal.XMLHttpRequest(); 215 | client.onreadystatechange = onreadystatechange; 216 | client.open("GET", "example.com/someApi"); 217 | client.setRequestHeader("Content-Type", "application/json"); 218 | client.send(); 219 | 220 | request = mockAjax.requests.mostRecent(); 221 | var responseObject = {status: 200, statusText: "OK", contentType: "application/json", responseText: '{"foo":"bar"}', responseType: 'json'}; 222 | 223 | request.respondWith(responseObject); 224 | 225 | sharedContext.responseCallback = success; 226 | sharedContext.status = responseObject.status; 227 | sharedContext.statusText = responseObject.statusText; 228 | sharedContext.contentType = responseObject.contentType; 229 | sharedContext.responseText = responseObject.responseText; 230 | sharedContext.responseType = responseObject.responseType; 231 | sharedContext.responseURL = responseObject.responseURL; 232 | 233 | response = success.calls.mostRecent().args[2]; 234 | }); 235 | 236 | it("should return the provided override for the XHR2 response", function() { 237 | var responseText = sharedContext.responseText; 238 | 239 | expect(response.responseText).toEqual(responseText); 240 | expect(response.response).toEqual({foo: "bar"}); 241 | }); 242 | 243 | sharedAjaxResponseBehaviorForZepto_Success(sharedContext); 244 | }); 245 | 246 | describe("and the response is Success, and responseURL is set", function () { 247 | beforeEach(function() { 248 | client = new fakeGlobal.XMLHttpRequest(); 249 | client.onreadystatechange = onreadystatechange; 250 | client.open("GET", "example.com/someApi"); 251 | client.setRequestHeader("Content-Type", "application/json"); 252 | client.send(); 253 | 254 | request = mockAjax.requests.mostRecent(); 255 | var responseObject = {status: 200, statusText: "OK", contentType: "application/json", responseText: '{"foo":"bar"}', responseType: 'json', responseURL: 'example.com/redirected'}; 256 | 257 | request.respondWith(responseObject); 258 | 259 | sharedContext.responseCallback = success; 260 | sharedContext.status = responseObject.status; 261 | sharedContext.statusText = responseObject.statusText; 262 | sharedContext.contentType = responseObject.contentType; 263 | sharedContext.responseText = responseObject.responseText; 264 | sharedContext.responseType = responseObject.responseType; 265 | sharedContext.responseURL = responseObject.responseURL; 266 | 267 | response = success.calls.mostRecent().args[2]; 268 | }); 269 | 270 | sharedAjaxResponseBehaviorForZepto_Success(sharedContext); 271 | }); 272 | 273 | describe("response with unique header names using an object", function () { 274 | beforeEach(function () { 275 | client = new fakeGlobal.XMLHttpRequest(); 276 | client.onreadystatechange = onreadystatechange; 277 | client.open("GET", "example.com"); 278 | client.send(); 279 | 280 | request = mockAjax.requests.mostRecent(); 281 | var responseObject = {status: 200, statusText: "OK", responseText: '["foo"]', responseHeaders: { 282 | 'X-Header1': 'header 1 value', 283 | 'X-Header2': 'header 2 value', 284 | 'X-Header3': 'header 3 value' 285 | }}; 286 | request.respondWith(responseObject); 287 | response = success.calls.mostRecent().args[2]; 288 | }); 289 | 290 | it("getResponseHeader should return the each value", function () { 291 | expect(response.getResponseHeader('X-Header1')).toBe('header 1 value'); 292 | expect(response.getResponseHeader('X-Header2')).toBe('header 2 value'); 293 | expect(response.getResponseHeader('X-Header3')).toBe('header 3 value'); 294 | }); 295 | 296 | it("getAllResponseHeaders should return all values", function () { 297 | expect(response.getAllResponseHeaders()).toBe([ 298 | "X-Header1: header 1 value", 299 | "X-Header2: header 2 value", 300 | "X-Header3: header 3 value" 301 | ].join("\r\n") + "\r\n"); 302 | }); 303 | }); 304 | 305 | describe("response with multiple headers of the same name using an array of objects", function () { 306 | beforeEach(function () { 307 | client = new fakeGlobal.XMLHttpRequest(); 308 | client.onreadystatechange = onreadystatechange; 309 | client.open("GET", "example.com"); 310 | client.send(); 311 | 312 | request = mockAjax.requests.mostRecent(); 313 | var responseObject = {status: 200, statusText: "OK", responseText: '["foo"]', responseHeaders: [ 314 | { name: 'X-Header', value: 'header value 1' }, 315 | { name: 'X-Header', value: 'header value 2' } 316 | ]}; 317 | request.respondWith(responseObject); 318 | response = success.calls.mostRecent().args[2]; 319 | }); 320 | 321 | it("getResponseHeader should return all values comma separated", function () { 322 | expect(response.getResponseHeader('X-Header')).toBe('header value 1, header value 2'); 323 | }); 324 | 325 | it("returns null, if header does not exist", function() { 326 | expect(response.getResponseHeader('X-Does-Not-Exist')).toBe(null); 327 | }); 328 | 329 | it("getAllResponseHeaders should return all values", function () { 330 | expect(response.getAllResponseHeaders()).toBe([ 331 | "X-Header: header value 1", 332 | "X-Header: header value 2" 333 | ].join("\r\n") + "\r\n"); 334 | }); 335 | }); 336 | 337 | describe("the content type defaults to application/json", function () { 338 | beforeEach(function() { 339 | client = new fakeGlobal.XMLHttpRequest(); 340 | client.onreadystatechange = onreadystatechange; 341 | client.open("GET", "example.com/someApi"); 342 | client.setRequestHeader("Content-Type", "application/json"); 343 | client.send(); 344 | 345 | request = mockAjax.requests.mostRecent(); 346 | response = {status: 200, statusText: "OK", responseText: '{"foo": "valid JSON, dammit."}', responseType: 'json'}; 347 | request.respondWith(response); 348 | 349 | sharedContext.responseCallback = success; 350 | sharedContext.status = response.status; 351 | sharedContext.statusText = response.statusText; 352 | sharedContext.contentType = "application/json"; 353 | sharedContext.responseType = response.responseType; 354 | sharedContext.responseText = response.responseText; 355 | sharedContext.responseURL = response.responseURL; 356 | }); 357 | 358 | it("should call the success handler", function() { 359 | expect(success).toHaveBeenCalled(); 360 | }); 361 | 362 | it("should not call the failure handler", function() { 363 | expect(error).not.toHaveBeenCalled(); 364 | }); 365 | 366 | it("should call the complete handler", function() { 367 | expect(complete).toHaveBeenCalled(); 368 | }); 369 | 370 | sharedAjaxResponseBehaviorForZepto_Success(sharedContext); 371 | }); 372 | 373 | describe("and the status/response code is 0", function () { 374 | beforeEach(function() { 375 | client = new fakeGlobal.XMLHttpRequest(); 376 | client.onreadystatechange = onreadystatechange; 377 | client.open("GET", "example.com/someApi"); 378 | client.setRequestHeader("Content-Type", "text/plain"); 379 | client.send(); 380 | 381 | request = mockAjax.requests.mostRecent(); 382 | response = {status: 0, statusText: "ABORT", responseText: '{"foo": "whoops!"}'}; 383 | request.respondWith(response); 384 | 385 | sharedContext.responseCallback = error; 386 | sharedContext.status = 0; 387 | sharedContext.statusText = response.statusText; 388 | sharedContext.contentType = 'application/json'; 389 | sharedContext.responseText = response.responseText; 390 | sharedContext.responseType = response.responseType; 391 | sharedContext.responseURL = response.responseURL; 392 | }); 393 | 394 | it("should call the success handler", function() { 395 | expect(success).not.toHaveBeenCalled(); 396 | }); 397 | 398 | it("should not call the failure handler", function() { 399 | expect(error).toHaveBeenCalled(); 400 | }); 401 | 402 | it("should call the complete handler", function() { 403 | expect(complete).toHaveBeenCalled(); 404 | }); 405 | 406 | sharedAjaxResponseBehaviorForZepto_Failure(sharedContext); 407 | }); 408 | }); 409 | 410 | describe("and the response is error", function () { 411 | beforeEach(function() { 412 | client = new fakeGlobal.XMLHttpRequest(); 413 | client.onreadystatechange = onreadystatechange; 414 | client.open("GET", "example.com/someApi"); 415 | client.setRequestHeader("Content-Type", "text/plain"); 416 | client.send(); 417 | 418 | request = mockAjax.requests.mostRecent(); 419 | response = {status: 500, statusText: "SERVER ERROR", contentType: "text/html", responseText: "(._){"}; 420 | request.respondWith(response); 421 | 422 | sharedContext.responseCallback = error; 423 | sharedContext.status = response.status; 424 | sharedContext.statusText = response.statusText; 425 | sharedContext.contentType = response.contentType; 426 | sharedContext.responseText = response.responseText; 427 | sharedContext.responseType = response.responseType; 428 | sharedContext.responseURL = response.responseURL; 429 | }); 430 | 431 | it("should not call the success handler", function() { 432 | expect(success).not.toHaveBeenCalled(); 433 | }); 434 | 435 | it("should call the failure handler", function() { 436 | expect(error).toHaveBeenCalled(); 437 | }); 438 | 439 | it("should call the complete handler", function() { 440 | expect(complete).toHaveBeenCalled(); 441 | }); 442 | 443 | sharedAjaxResponseBehaviorForZepto_Failure(sharedContext); 444 | }); 445 | 446 | describe('when simulating a response with request.responseTimeout', function() { 447 | beforeEach(function() { 448 | jasmine.clock().install(); 449 | 450 | client = new fakeGlobal.XMLHttpRequest(); 451 | client.onreadystatechange = onreadystatechange; 452 | client.open("GET", "example.com/someApi"); 453 | client.setRequestHeader("Content-Type", "text/plain"); 454 | client.send(); 455 | 456 | request = mockAjax.requests.mostRecent(); 457 | response = {contentType: "text/html", response: "(._){response", responseText: "(._){", responseType: "text"}; 458 | request.responseTimeout(response); 459 | 460 | sharedContext.responseCallback = error; 461 | sharedContext.status = response.status; 462 | sharedContext.statusText = response.statusText; 463 | sharedContext.contentType = response.contentType; 464 | sharedContext.responseText = response.responseText; 465 | sharedContext.responseType = response.responseType; 466 | sharedContext.responseURL = response.responseURL; 467 | }); 468 | 469 | afterEach(function() { 470 | jasmine.clock().uninstall(); 471 | }); 472 | 473 | it("should not call the success handler", function() { 474 | expect(success).not.toHaveBeenCalled(); 475 | }); 476 | 477 | it("should call the failure handler", function() { 478 | expect(error).toHaveBeenCalled(); 479 | }); 480 | 481 | it("should call the complete handler", function() { 482 | expect(complete).toHaveBeenCalled(); 483 | }); 484 | }); 485 | }); 486 | 487 | 488 | function sharedAjaxResponseBehaviorForZepto_Success(context) { 489 | describe("the success response", function () { 490 | var xhr; 491 | beforeEach(function() { 492 | xhr = context.responseCallback.calls.mostRecent().args[2]; 493 | }); 494 | 495 | it("should have the expected status code", function() { 496 | expect(xhr.status).toEqual(context.status); 497 | }); 498 | 499 | it("should have the expected content type", function() { 500 | expect(xhr.getResponseHeader('Content-Type')).toEqual(context.contentType); 501 | }); 502 | 503 | it("should have the expected xhr2 response", function() { 504 | var expected = context.response || context.responseType === 'json' ? JSON.parse(context.responseText) : context.responseText; 505 | expect(xhr.response).toEqual(expected); 506 | }); 507 | 508 | it("should have the expected response text", function() { 509 | expect(xhr.responseText).toEqual(context.responseText); 510 | }); 511 | 512 | it("should have the expected status text", function() { 513 | expect(xhr.statusText).toEqual(context.statusText); 514 | }); 515 | 516 | it("should have the expected response URL", function() { 517 | expect(xhr.responseURL).toEqual(context.responseURL || null); 518 | }); 519 | }); 520 | } 521 | 522 | function sharedAjaxResponseBehaviorForZepto_Failure(context) { 523 | describe("the failure response", function () { 524 | var xhr; 525 | beforeEach(function() { 526 | xhr = context.responseCallback.calls.mostRecent().args[0]; 527 | }); 528 | 529 | it("should have the expected status code", function() { 530 | expect(xhr.status).toEqual(context.status); 531 | }); 532 | 533 | it("should have the expected content type", function() { 534 | expect(xhr.getResponseHeader('Content-Type')).toEqual(context.contentType); 535 | }); 536 | 537 | it("should have the expected xhr2 response", function() { 538 | var expected = context.response || xhr.responseType === 'json' ? JSON.parse(xhr.responseText) : xhr.responseText; 539 | expect(xhr.response).toEqual(expected); 540 | }); 541 | 542 | it("should have the expected response text", function() { 543 | expect(xhr.responseText).toEqual(context.responseText); 544 | }); 545 | 546 | it("should have the expected status text", function() { 547 | expect(xhr.statusText).toEqual(context.statusText); 548 | }); 549 | 550 | it("should have the expected response URL", function() { 551 | expect(xhr.responseURL).toEqual(context.responseURL || null); 552 | }); 553 | }); 554 | } 555 | -------------------------------------------------------------------------------- /spec/paramParserSpec.js: -------------------------------------------------------------------------------- 1 | describe('ParamParser', function() { 2 | beforeEach(function() { 3 | var Constructor = getJasmineRequireObj().AjaxParamParser(); 4 | expect(Constructor).toEqual(jasmine.any(Function)); 5 | this.parser = new Constructor(); 6 | }); 7 | 8 | it('has a default parser', function() { 9 | var parser = this.parser.findParser({ contentType: function() {} }), 10 | parsed = parser.parse('3+stooges=shemp&3+stooges=larry%20%26%20moe%20%26%20curly&some%3Dthing=else+entirely'); 11 | 12 | expect(parsed).toEqual({ 13 | '3 stooges': ['shemp', 'larry & moe & curly'], 14 | 'some=thing': ['else entirely'] 15 | }); 16 | }); 17 | 18 | it('should detect and parse json', function() { 19 | var data = { 20 | foo: 'bar', 21 | baz: ['q', 'u', 'u', 'x'], 22 | nested: { 23 | object: { 24 | containing: 'stuff' 25 | } 26 | } 27 | }, 28 | parser = this.parser.findParser({ contentType: function() { return 'application/json'; } }), 29 | parsed = parser.parse(JSON.stringify(data)); 30 | 31 | expect(parsed).toEqual(data); 32 | }); 33 | 34 | it('should parse json with further qualifiers on content-type', function() { 35 | var data = { 36 | foo: 'bar', 37 | baz: ['q', 'u', 'u', 'x'], 38 | nested: { 39 | object: { 40 | containing: 'stuff' 41 | } 42 | } 43 | }, 44 | parser = this.parser.findParser({ contentType: function() { return 'application/json; charset=utf-8'; } }), 45 | parsed = parser.parse(JSON.stringify(data)); 46 | 47 | expect(parsed).toEqual(data); 48 | }); 49 | 50 | it('should have custom parsers take precedence', function() { 51 | var custom = { 52 | test: jasmine.createSpy('test').and.returnValue(true), 53 | parse: jasmine.createSpy('parse').and.returnValue('parsedFormat') 54 | }; 55 | 56 | this.parser.add(custom); 57 | 58 | var parser = this.parser.findParser({ contentType: function() {} }), 59 | parsed = parser.parse('custom_format'); 60 | 61 | expect(parsed).toEqual('parsedFormat'); 62 | expect(custom.test).toHaveBeenCalled(); 63 | expect(custom.parse).toHaveBeenCalledWith('custom_format'); 64 | }); 65 | 66 | it('should skip custom parsers that do not match', function() { 67 | var custom = { 68 | test: jasmine.createSpy('test').and.returnValue(false), 69 | parse: jasmine.createSpy('parse').and.returnValue('parsedFormat') 70 | }; 71 | 72 | this.parser.add(custom); 73 | 74 | var parser = this.parser.findParser({ contentType: function() {} }), 75 | parsed = parser.parse('custom_format'); 76 | 77 | expect(parsed).toEqual({ custom_format: [ 'undefined' ] }); 78 | expect(custom.test).toHaveBeenCalled(); 79 | expect(custom.parse).not.toHaveBeenCalled(); 80 | }); 81 | 82 | it('removes custom parsers when reset', function() { 83 | var custom = { 84 | test: jasmine.createSpy('test').and.returnValue(true), 85 | parse: jasmine.createSpy('parse').and.returnValue('parsedFormat') 86 | }; 87 | 88 | this.parser.add(custom); 89 | 90 | var parser = this.parser.findParser({ contentType: function() {} }), 91 | parsed = parser.parse('custom_format'); 92 | 93 | expect(parsed).toEqual('parsedFormat'); 94 | 95 | custom.test.calls.reset(); 96 | custom.parse.calls.reset(); 97 | 98 | this.parser.reset(); 99 | 100 | parser = this.parser.findParser({ contentType: function() {} }); 101 | parsed = parser.parse('custom_format'); 102 | 103 | expect(parsed).toEqual({ custom_format: [ 'undefined' ] }); 104 | expect(custom.test).not.toHaveBeenCalled(); 105 | expect(custom.parse).not.toHaveBeenCalled(); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /spec/requestStubSpec.js: -------------------------------------------------------------------------------- 1 | describe('RequestStub', function() { 2 | beforeEach(function() { 3 | this.RequestStub = getJasmineRequireObj().AjaxRequestStub(); 4 | 5 | jasmine.addMatchers({ 6 | toMatchRequest: function() { 7 | return { 8 | compare: function(actual) { 9 | return { 10 | pass: actual.matches.apply(actual, Array.prototype.slice.call(arguments, 1)) 11 | }; 12 | } 13 | }; 14 | } 15 | }); 16 | }); 17 | 18 | it('matches just by exact url', function() { 19 | var stub = new this.RequestStub('www.example.com/foo'); 20 | 21 | expect(stub).toMatchRequest('www.example.com/foo'); 22 | }); 23 | 24 | it('does not match if the url differs', function() { 25 | var stub = new this.RequestStub('www.example.com/foo'); 26 | 27 | expect(stub).not.toMatchRequest('www.example.com/bar'); 28 | }); 29 | 30 | it('matches unordered query params', function() { 31 | var stub = new this.RequestStub('www.example.com?foo=bar&baz=quux'); 32 | 33 | expect(stub).toMatchRequest('www.example.com?baz=quux&foo=bar'); 34 | }); 35 | 36 | it('requires all specified query params to be there', function() { 37 | var stub = new this.RequestStub('www.example.com?foo=bar&baz=quux'); 38 | 39 | expect(stub).not.toMatchRequest('www.example.com?foo=bar'); 40 | }); 41 | 42 | it('can match the url with a RegExp', function() { 43 | var stub = new this.RequestStub(/ba[rz]/); 44 | 45 | expect(stub).toMatchRequest('bar'); 46 | expect(stub).toMatchRequest('baz'); 47 | expect(stub).not.toMatchRequest('foo'); 48 | }); 49 | 50 | it('requires the method to match if supplied', function() { 51 | var stub = new this.RequestStub('www.example.com/foo', null, 'POST'); 52 | 53 | expect(stub).not.toMatchRequest('www.example.com/foo'); 54 | expect(stub).not.toMatchRequest('www.example.com/foo', null, 'GET'); 55 | expect(stub).toMatchRequest('www.example.com/foo', null, 'POST'); 56 | }); 57 | 58 | it('requires the data submitted to match if supplied', function() { 59 | var stub = new this.RequestStub('/foo', 'foo=bar&baz=quux'); 60 | 61 | expect(stub).toMatchRequest('/foo', 'baz=quux&foo=bar'); 62 | expect(stub).not.toMatchRequest('/foo', 'foo=bar'); 63 | }); 64 | 65 | it('can match the data or query params with a RegExp', function() { 66 | var stub = new this.RequestStub('/foo', /ba[rz]=quux/); 67 | 68 | expect(stub).toMatchRequest('/foo', 'bar=quux'); 69 | expect(stub).toMatchRequest('/foo', 'baz=quux'); 70 | expect(stub).not.toMatchRequest('/foo', 'foo=bar'); 71 | }); 72 | 73 | describe('when returning successfully', function() { 74 | it('passes response information to the request', function() { 75 | var stub = new this.RequestStub('/foo'); 76 | stub.andReturn({ 77 | status: 300, 78 | statusText: 'hi there', 79 | contentType: 'text/plain', 80 | extra: 'stuff' 81 | }); 82 | var fakeRequest = { respondWith: jasmine.createSpy('respondWith') }; 83 | 84 | stub.handleRequest(fakeRequest); 85 | 86 | expect(fakeRequest.respondWith).toHaveBeenCalledWith({ 87 | status: 300, 88 | statusText: 'hi there', 89 | contentType: 'text/plain', 90 | extra: 'stuff' 91 | }); 92 | }); 93 | 94 | it('defaults to status 200', function() { 95 | var stub = new this.RequestStub('/foo'); 96 | stub.andReturn({}); 97 | var fakeRequest = { respondWith: jasmine.createSpy('respondWith') }; 98 | 99 | stub.handleRequest(fakeRequest); 100 | 101 | expect(fakeRequest.respondWith).toHaveBeenCalledWith(jasmine.objectContaining({ 102 | status: 200 103 | })); 104 | }); 105 | 106 | it('allows setting a response code of 0', function() { 107 | var stub = new this.RequestStub('/foo'); 108 | stub.andReturn({status: 0}); 109 | var fakeRequest = { respondWith: jasmine.createSpy('respondWith') }; 110 | 111 | stub.handleRequest(fakeRequest); 112 | 113 | expect(fakeRequest.respondWith).toHaveBeenCalledWith(jasmine.objectContaining({ 114 | status: 0 115 | })); 116 | }); 117 | }); 118 | 119 | describe('when erroring', function() { 120 | it('passes error information to request', function() { 121 | var stub = new this.RequestStub('/foo'); 122 | stub.andError({ 123 | status: 502, 124 | extra: 'stuff' 125 | }); 126 | 127 | var fakeRequest = { responseError: jasmine.createSpy('responseError') }; 128 | stub.handleRequest(fakeRequest); 129 | 130 | expect(fakeRequest.responseError).toHaveBeenCalledWith({ 131 | status: 502, 132 | extra: 'stuff' 133 | }); 134 | }); 135 | 136 | it('defaults to status 500', function() { 137 | var stub = new this.RequestStub('/foo'); 138 | stub.andError({}); 139 | 140 | var fakeRequest = { responseError: jasmine.createSpy('responseError') }; 141 | stub.handleRequest(fakeRequest); 142 | 143 | expect(fakeRequest.responseError).toHaveBeenCalledWith(jasmine.objectContaining({ 144 | status: 500 145 | })); 146 | }); 147 | }); 148 | 149 | describe('when timing out', function() { 150 | it('tells the request to time out', function() { 151 | var stub = new this.RequestStub('/foo'); 152 | stub.andTimeout(); 153 | 154 | var fakeRequest = { responseTimeout: jasmine.createSpy('responseTimeout') }; 155 | stub.handleRequest(fakeRequest); 156 | 157 | expect(fakeRequest.responseTimeout).toHaveBeenCalled(); 158 | }); 159 | }); 160 | 161 | describe('when calling a function', function() { 162 | it('invokes the function with the request', function() { 163 | var stub = new this.RequestStub('/foo'); 164 | var callback = jasmine.createSpy('callback').and.returnValue({ status: 201 }); 165 | stub.andCallFunction(callback); 166 | 167 | var fakeRequest = { things: 'stuff' }; 168 | stub.handleRequest(fakeRequest); 169 | 170 | expect(callback).toHaveBeenCalledWith(fakeRequest); 171 | }); 172 | }); 173 | }); 174 | -------------------------------------------------------------------------------- /spec/requestTrackerSpec.js: -------------------------------------------------------------------------------- 1 | describe('RequestTracker', function() { 2 | beforeEach(function() { 3 | var Constructor = getJasmineRequireObj().AjaxRequestTracker(); 4 | this.tracker = new Constructor(); 5 | }); 6 | 7 | it('tracks the number of times ajax requests are made', function() { 8 | expect(this.tracker.count()).toBe(0); 9 | 10 | this.tracker.track(); 11 | 12 | expect(this.tracker.count()).toBe(1); 13 | }); 14 | 15 | it('simplifies access to the last (most recent) request', function() { 16 | this.tracker.track(); 17 | this.tracker.track('request'); 18 | 19 | expect(this.tracker.mostRecent()).toEqual('request'); 20 | }); 21 | 22 | it('returns a useful falsy value when there is no last (most recent) request', function() { 23 | expect(this.tracker.mostRecent()).toBeFalsy(); 24 | }); 25 | 26 | it('simplifies access to the first (oldest) request', function() { 27 | this.tracker.track('request'); 28 | this.tracker.track(); 29 | 30 | expect(this.tracker.first()).toEqual('request'); 31 | }); 32 | 33 | it('returns a useful falsy value when there is no first (oldest) request', function() { 34 | expect(this.tracker.first()).toBeFalsy(); 35 | }); 36 | 37 | it('allows the requests list to be reset', function() { 38 | this.tracker.track(); 39 | this.tracker.track(); 40 | 41 | expect(this.tracker.count()).toBe(2); 42 | 43 | this.tracker.reset(); 44 | 45 | expect(this.tracker.count()).toBe(0); 46 | }); 47 | 48 | it('allows retrieval of an arbitrary request by index', function() { 49 | this.tracker.track('1'); 50 | this.tracker.track('2'); 51 | this.tracker.track('3'); 52 | 53 | expect(this.tracker.at(1)).toEqual('2'); 54 | }); 55 | 56 | it('allows retrieval of all requests that are for a given url', function() { 57 | this.tracker.track({ url: 'foo' }); 58 | this.tracker.track({ url: 'bar' }); 59 | 60 | expect(this.tracker.filter('bar')).toEqual([{ url: 'bar' }]); 61 | }); 62 | 63 | it('allows retrieval of all requests that match a given RegExp', function() { 64 | this.tracker.track({ url: 'foo' }); 65 | this.tracker.track({ url: 'bar' }); 66 | this.tracker.track({ url: 'baz' }); 67 | 68 | expect(this.tracker.filter(/ba[rz]/)).toEqual([{ url: 'bar' }, { url: 'baz' }]); 69 | }); 70 | 71 | it('allows retrieval of all requests that match based on a function', function() { 72 | this.tracker.track({ url: 'foo' }); 73 | this.tracker.track({ url: 'bar' }); 74 | this.tracker.track({ url: 'baz' }); 75 | 76 | var func = function(request) { 77 | return request.url === 'bar'; 78 | }; 79 | 80 | expect(this.tracker.filter(func)).toEqual([{ url: 'bar' }]); 81 | }); 82 | 83 | it('filters to nothing if no requests have been tracked', function() { 84 | expect(this.tracker.filter('foo')).toEqual([]); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /spec/stubTrackerSpec.js: -------------------------------------------------------------------------------- 1 | describe('StubTracker', function() { 2 | beforeEach(function() { 3 | var Constructor = getJasmineRequireObj().AjaxStubTracker(); 4 | this.tracker = new Constructor(); 5 | }); 6 | 7 | it('finds nothing if no stubs are added', function() { 8 | expect(this.tracker.findStub()).toBeUndefined(); 9 | }); 10 | 11 | it('finds an added stub', function() { 12 | var stub = { matches: function() { return true; } }; 13 | this.tracker.addStub(stub); 14 | 15 | expect(this.tracker.findStub()).toBe(stub); 16 | }); 17 | 18 | it('skips an added stub that does not match', function() { 19 | var stub = { matches: function() { return false; } }; 20 | this.tracker.addStub(stub); 21 | 22 | expect(this.tracker.findStub()).toBeUndefined(); 23 | }); 24 | 25 | it('passes url, data, and method to the stub', function() { 26 | var stub = { matches: jasmine.createSpy('matches') }; 27 | this.tracker.addStub(stub); 28 | 29 | this.tracker.findStub('url', 'data', 'method'); 30 | 31 | expect(stub.matches).toHaveBeenCalledWith('url', 'data', 'method'); 32 | }); 33 | 34 | it('can clear out all stubs', function() { 35 | var stub = { matches: jasmine.createSpy('matches') }; 36 | this.tracker.addStub(stub); 37 | 38 | this.tracker.findStub(); 39 | 40 | expect(stub.matches).toHaveBeenCalled(); 41 | 42 | this.tracker.reset(); 43 | stub.matches.calls.reset(); 44 | 45 | this.tracker.findStub(); 46 | 47 | expect(stub.matches).not.toHaveBeenCalled(); 48 | }); 49 | 50 | it('uses the most recently added stub that matches', function() { 51 | var stub1 = { matches: function() { return true; } }; 52 | var stub2 = { matches: function() { return true; } }; 53 | var stub3 = { matches: function() { return false; } }; 54 | 55 | this.tracker.addStub(stub1); 56 | this.tracker.addStub(stub2); 57 | this.tracker.addStub(stub3); 58 | 59 | expect(this.tracker.findStub()).toBe(stub2); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /spec/support/jasmine-browser.js: -------------------------------------------------------------------------------- 1 | // jshint ignore:start 2 | module.exports = { 3 | srcDir: 'src', 4 | srcFiles: [ 5 | 'requireAjax.js', 6 | '[^b]*.js', 7 | 'boot.js' 8 | ], 9 | specDir: 'spec', 10 | specFiles: ['**/*[Ss]pec.js'], 11 | helpers: [ 12 | 'helpers/spec-helper.js' 13 | ], 14 | random: true, 15 | browser: { 16 | name: process.env.JASMINE_BROWSER || 'firefox', 17 | useRemoteSeleniumGrid: process.env.USE_SAUCE === 'true', 18 | remoteSeleniumGrid: { 19 | url: 'https://ondemand.saucelabs.com/wd/hub', 20 | browserVersion: process.env.SAUCE_BROWSER_VERSION, 21 | platformName: process.env.SAUCE_OS, 22 | 'sauce:options': { 23 | name: `jasmine-ajax ${new Date().toISOString()}`, 24 | build: `jasmine-ajax ${process.env.CIRCLE_WORKFLOW_ID || 'Ran locally'}`, 25 | tags: ['jasmine-ajax'], 26 | tunnelName: process.env.SAUCE_TUNNEL_NAME, 27 | username: process.env.SAUCE_USERNAME, 28 | accessKey: process.env.SAUCE_ACCESS_KEY 29 | } 30 | } 31 | } 32 | }; -------------------------------------------------------------------------------- /src/boot.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Jasmine-Ajax - v<%= packageVersion %>: a set of helpers for testing AJAX requests under the Jasmine 4 | BDD framework for JavaScript. 5 | 6 | http://github.com/jasmine/jasmine-ajax 7 | 8 | Jasmine Home page: http://jasmine.github.io/ 9 | 10 | Copyright (c) 2008-2015 Pivotal Labs 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | */ 32 | 33 | //Module wrapper to support both browser and CommonJS environment 34 | (function (root, factory) { 35 | // if (typeof exports === 'object' && typeof exports.nodeName !== 'string') { 36 | // // CommonJS 37 | // var jasmineRequire = require('jasmine-core'); 38 | // module.exports = factory(root, function() { 39 | // return jasmineRequire; 40 | // }); 41 | // } else { 42 | // Browser globals 43 | window.MockAjax = factory(root, getJasmineRequireObj); 44 | // } 45 | }(typeof window !== 'undefined' ? window : global, function (global, getJasmineRequireObj) { 46 | 47 | // <% files.forEach(function(filename) { %> 48 | // include "<%= filename %>";<% }); %> 49 | 50 | var jRequire = getJasmineRequireObj(); 51 | var MockAjax = jRequire.ajax(jRequire); 52 | jasmine.Ajax = new MockAjax(global); 53 | 54 | return MockAjax; 55 | })); 56 | -------------------------------------------------------------------------------- /src/event.js: -------------------------------------------------------------------------------- 1 | getJasmineRequireObj().AjaxEvent = function() { 2 | function now() { 3 | return new Date().getTime(); 4 | } 5 | 6 | function noop() { 7 | } 8 | 9 | // Event object 10 | // https://dom.spec.whatwg.org/#concept-event 11 | function XMLHttpRequestEvent(xhr, type) { 12 | this.type = type; 13 | this.bubbles = false; 14 | this.cancelable = false; 15 | this.timeStamp = now(); 16 | 17 | this.isTrusted = false; 18 | this.defaultPrevented = false; 19 | 20 | // Event phase should be "AT_TARGET" 21 | // https://dom.spec.whatwg.org/#dom-event-at_target 22 | this.eventPhase = 2; 23 | 24 | this.target = xhr; 25 | this.currentTarget = xhr; 26 | } 27 | 28 | XMLHttpRequestEvent.prototype.preventDefault = noop; 29 | XMLHttpRequestEvent.prototype.stopPropagation = noop; 30 | XMLHttpRequestEvent.prototype.stopImmediatePropagation = noop; 31 | 32 | function XMLHttpRequestProgressEvent() { 33 | XMLHttpRequestEvent.apply(this, arguments); 34 | 35 | this.lengthComputable = false; 36 | this.loaded = 0; 37 | this.total = 0; 38 | } 39 | 40 | // Extend prototype 41 | XMLHttpRequestProgressEvent.prototype = XMLHttpRequestEvent.prototype; 42 | 43 | return { 44 | event: function(xhr, type) { 45 | return new XMLHttpRequestEvent(xhr, type); 46 | }, 47 | 48 | progressEvent: function(xhr, type) { 49 | return new XMLHttpRequestProgressEvent(xhr, type); 50 | } 51 | }; 52 | }; -------------------------------------------------------------------------------- /src/eventBus.js: -------------------------------------------------------------------------------- 1 | getJasmineRequireObj().AjaxEventBus = function(eventFactory) { 2 | function EventBus(source) { 3 | this.eventList = {}; 4 | this.source = source; 5 | } 6 | 7 | function ensureEvent(eventList, name) { 8 | eventList[name] = eventList[name] || []; 9 | return eventList[name]; 10 | } 11 | 12 | function findIndex(list, thing) { 13 | if (list.indexOf) { 14 | return list.indexOf(thing); 15 | } 16 | 17 | for(var i = 0; i < list.length; i++) { 18 | if (thing === list[i]) { 19 | return i; 20 | } 21 | } 22 | 23 | return -1; 24 | } 25 | 26 | EventBus.prototype.addEventListener = function(event, callback) { 27 | ensureEvent(this.eventList, event).push(callback); 28 | }; 29 | 30 | EventBus.prototype.removeEventListener = function(event, callback) { 31 | var index = findIndex(this.eventList[event], callback); 32 | 33 | if (index >= 0) { 34 | this.eventList[event].splice(index, 1); 35 | } 36 | }; 37 | 38 | EventBus.prototype.trigger = function(event) { 39 | var evt; 40 | 41 | // Event 'readystatechange' is should be a simple event. 42 | // Others are progress event. 43 | // https://xhr.spec.whatwg.org/#events 44 | if (event === 'readystatechange') { 45 | evt = eventFactory.event(this.source, event); 46 | } else { 47 | evt = eventFactory.progressEvent(this.source, event); 48 | } 49 | 50 | var eventListeners = this.eventList[event]; 51 | 52 | if (eventListeners) { 53 | for (var i = 0; i < eventListeners.length; i++) { 54 | eventListeners[i].call(this.source, evt); 55 | } 56 | } 57 | }; 58 | 59 | return function(source) { 60 | return new EventBus(source); 61 | }; 62 | }; 63 | -------------------------------------------------------------------------------- /src/fakeRequest.js: -------------------------------------------------------------------------------- 1 | // jshint latedef: nofunc 2 | getJasmineRequireObj().AjaxFakeRequest = function(eventBusFactory) { 3 | function extend(destination, source, propertiesToSkip) { 4 | propertiesToSkip = propertiesToSkip || []; 5 | for (var property in source) { 6 | if (!arrayContains(propertiesToSkip, property)) { 7 | destination[property] = source[property]; 8 | } 9 | } 10 | return destination; 11 | } 12 | 13 | function arrayContains(arr, item) { 14 | for (var i = 0; i < arr.length; i++) { 15 | if (arr[i] === item) { 16 | return true; 17 | } 18 | } 19 | return false; 20 | } 21 | 22 | function wrapProgressEvent(xhr, eventName) { 23 | return function() { 24 | if (xhr[eventName]) { 25 | xhr[eventName].apply(xhr, arguments); 26 | } 27 | }; 28 | } 29 | 30 | function initializeEvents(xhr) { 31 | xhr.eventBus.addEventListener('readystatechange', wrapProgressEvent(xhr, 'onreadystatechange')); 32 | xhr.eventBus.addEventListener('loadstart', wrapProgressEvent(xhr, 'onloadstart')); 33 | xhr.eventBus.addEventListener('load', wrapProgressEvent(xhr, 'onload')); 34 | xhr.eventBus.addEventListener('loadend', wrapProgressEvent(xhr, 'onloadend')); 35 | xhr.eventBus.addEventListener('progress', wrapProgressEvent(xhr, 'onprogress')); 36 | xhr.eventBus.addEventListener('error', wrapProgressEvent(xhr, 'onerror')); 37 | xhr.eventBus.addEventListener('abort', wrapProgressEvent(xhr, 'onabort')); 38 | xhr.eventBus.addEventListener('timeout', wrapProgressEvent(xhr, 'ontimeout')); 39 | } 40 | 41 | function unconvertibleResponseTypeMessage(type) { 42 | var msg = [ 43 | "Can't build XHR.response for XHR.responseType of '", 44 | type, 45 | "'.", 46 | "XHR.response must be explicitly stubbed" 47 | ]; 48 | return msg.join(' '); 49 | } 50 | 51 | function fakeRequest(global, requestTracker, stubTracker, paramParser) { 52 | function FakeXMLHttpRequest() { 53 | requestTracker.track(this); 54 | this.eventBus = eventBusFactory(this); 55 | initializeEvents(this); 56 | this.requestHeaders = {}; 57 | this.overriddenMimeType = null; 58 | } 59 | 60 | function findHeader(name, headers) { 61 | name = name.toLowerCase(); 62 | for (var header in headers) { 63 | if (header.toLowerCase() === name) { 64 | return headers[header]; 65 | } 66 | } 67 | } 68 | 69 | function normalizeHeaders(rawHeaders, contentType) { 70 | var headers = []; 71 | 72 | if (rawHeaders) { 73 | if (rawHeaders instanceof Array) { 74 | headers = rawHeaders; 75 | } else { 76 | for (var headerName in rawHeaders) { 77 | if (rawHeaders.hasOwnProperty(headerName)) { 78 | headers.push({ name: headerName, value: rawHeaders[headerName] }); 79 | } 80 | } 81 | } 82 | } else { 83 | headers.push({ name: "Content-Type", value: contentType || "application/json" }); 84 | } 85 | 86 | return headers; 87 | } 88 | 89 | function parseXml(xmlText, contentType) { 90 | if (global.DOMParser) { 91 | return (new global.DOMParser()).parseFromString(xmlText, 'text/xml'); 92 | } else { 93 | var xml = new global.ActiveXObject("Microsoft.XMLDOM"); 94 | xml.async = "false"; 95 | xml.loadXML(xmlText); 96 | return xml; 97 | } 98 | } 99 | 100 | var xmlParsables = ['text/xml', 'application/xml']; 101 | 102 | function getResponseXml(responseText, contentType) { 103 | if (arrayContains(xmlParsables, contentType.toLowerCase())) { 104 | return parseXml(responseText, contentType); 105 | } else if (contentType.match(/\+xml$/)) { 106 | return parseXml(responseText, 'text/xml'); 107 | } 108 | return null; 109 | } 110 | 111 | extend(FakeXMLHttpRequest, { 112 | UNSENT: 0, 113 | OPENED: 1, 114 | HEADERS_RECEIVED: 2, 115 | LOADING: 3, 116 | DONE: 4 117 | }); 118 | 119 | var iePropertiesThatCannotBeCopied = ['responseBody', 'responseText', 'responseXML', 'status', 'statusText', 'responseTimeout', 'responseURL']; 120 | extend(FakeXMLHttpRequest.prototype, new global.XMLHttpRequest(), iePropertiesThatCannotBeCopied); 121 | extend(FakeXMLHttpRequest.prototype, { 122 | open: function() { 123 | this.method = arguments[0]; 124 | this.url = arguments[1] + ''; 125 | this.username = arguments[3]; 126 | this.password = arguments[4]; 127 | this.readyState = FakeXMLHttpRequest.OPENED; 128 | this.requestHeaders = {}; 129 | this.eventBus.trigger('readystatechange'); 130 | }, 131 | 132 | setRequestHeader: function(header, value) { 133 | if (this.readyState === 0) { 134 | throw new Error('DOMException: Failed to execute "setRequestHeader" on "XMLHttpRequest": The object\'s state must be OPENED.'); 135 | } 136 | 137 | if(this.requestHeaders.hasOwnProperty(header)) { 138 | this.requestHeaders[header] = [this.requestHeaders[header], value].join(', '); 139 | } else { 140 | this.requestHeaders[header] = value; 141 | } 142 | }, 143 | 144 | overrideMimeType: function(mime) { 145 | this.overriddenMimeType = mime; 146 | }, 147 | 148 | abort: function() { 149 | this.readyState = FakeXMLHttpRequest.UNSENT; 150 | this.status = 0; 151 | this.statusText = "abort"; 152 | this.eventBus.trigger('readystatechange'); 153 | this.eventBus.trigger('progress'); 154 | this.eventBus.trigger('abort'); 155 | this.eventBus.trigger('loadend'); 156 | }, 157 | 158 | readyState: FakeXMLHttpRequest.UNSENT, 159 | 160 | onloadstart: null, 161 | onprogress: null, 162 | onabort: null, 163 | onerror: null, 164 | onload: null, 165 | ontimeout: null, 166 | onloadend: null, 167 | onreadystatechange: null, 168 | 169 | addEventListener: function() { 170 | this.eventBus.addEventListener.apply(this.eventBus, arguments); 171 | }, 172 | 173 | removeEventListener: function(event, callback) { 174 | this.eventBus.removeEventListener.apply(this.eventBus, arguments); 175 | }, 176 | 177 | status: null, 178 | 179 | send: function(data) { 180 | this.params = data; 181 | this.eventBus.trigger('loadstart'); 182 | 183 | var stub = stubTracker.findStub(this.url, data, this.method); 184 | if (stub) { 185 | stub.handleRequest(this); 186 | } 187 | }, 188 | 189 | contentType: function() { 190 | return findHeader('content-type', this.requestHeaders); 191 | }, 192 | 193 | data: function() { 194 | if (!this.params) { 195 | return {}; 196 | } 197 | 198 | return paramParser.findParser(this).parse(this.params); 199 | }, 200 | 201 | getResponseHeader: function(name) { 202 | var resultHeader = null; 203 | if (!this.responseHeaders) { return resultHeader; } 204 | 205 | name = name.toLowerCase(); 206 | for(var i = 0; i < this.responseHeaders.length; i++) { 207 | var header = this.responseHeaders[i]; 208 | if (name === header.name.toLowerCase()) { 209 | if (resultHeader) { 210 | resultHeader = [resultHeader, header.value].join(', '); 211 | } else { 212 | resultHeader = header.value; 213 | } 214 | } 215 | } 216 | return resultHeader; 217 | }, 218 | 219 | getAllResponseHeaders: function() { 220 | if (!this.responseHeaders) { return null; } 221 | 222 | var responseHeaders = []; 223 | for (var i = 0; i < this.responseHeaders.length; i++) { 224 | responseHeaders.push(this.responseHeaders[i].name + ': ' + 225 | this.responseHeaders[i].value); 226 | } 227 | return responseHeaders.join('\r\n') + '\r\n'; 228 | }, 229 | 230 | responseText: null, 231 | response: null, 232 | responseType: null, 233 | responseURL: null, 234 | 235 | responseValue: function() { 236 | switch(this.responseType) { 237 | case null: 238 | case "": 239 | case "text": 240 | return this.readyState >= FakeXMLHttpRequest.LOADING ? this.responseText : ""; 241 | case "json": 242 | return JSON.parse(this.responseText); 243 | case "arraybuffer": 244 | throw unconvertibleResponseTypeMessage('arraybuffer'); 245 | case "blob": 246 | throw unconvertibleResponseTypeMessage('blob'); 247 | case "document": 248 | return this.responseXML; 249 | } 250 | }, 251 | 252 | 253 | respondWith: function(response) { 254 | if (this.readyState === FakeXMLHttpRequest.DONE) { 255 | throw new Error("FakeXMLHttpRequest already completed"); 256 | } 257 | 258 | this.status = response.status; 259 | this.statusText = response.statusText || ""; 260 | this.responseHeaders = normalizeHeaders(response.responseHeaders, response.contentType); 261 | this.readyState = FakeXMLHttpRequest.HEADERS_RECEIVED; 262 | this.eventBus.trigger('readystatechange'); 263 | 264 | this.responseText = response.responseText || ""; 265 | this.responseType = response.responseType || ""; 266 | this.responseURL = response.responseURL || null; 267 | this.readyState = FakeXMLHttpRequest.DONE; 268 | this.responseXML = getResponseXml(response.responseText, this.getResponseHeader('content-type') || ''); 269 | if (this.responseXML) { 270 | this.responseType = 'document'; 271 | } 272 | if (response.responseJSON) { 273 | this.responseText = JSON.stringify(response.responseJSON); 274 | } 275 | 276 | if ('response' in response) { 277 | this.response = response.response; 278 | } else { 279 | this.response = this.responseValue(); 280 | } 281 | 282 | this.eventBus.trigger('readystatechange'); 283 | this.eventBus.trigger('progress'); 284 | this.eventBus.trigger('load'); 285 | this.eventBus.trigger('loadend'); 286 | }, 287 | 288 | responseTimeout: function() { 289 | if (this.readyState === FakeXMLHttpRequest.DONE) { 290 | throw new Error("FakeXMLHttpRequest already completed"); 291 | } 292 | this.readyState = FakeXMLHttpRequest.DONE; 293 | jasmine.clock().tick(30000); 294 | this.eventBus.trigger('readystatechange'); 295 | this.eventBus.trigger('progress'); 296 | this.eventBus.trigger('timeout'); 297 | this.eventBus.trigger('loadend'); 298 | }, 299 | 300 | responseError: function(response) { 301 | if (!response) { 302 | response = {}; 303 | } 304 | if (this.readyState === FakeXMLHttpRequest.DONE) { 305 | throw new Error("FakeXMLHttpRequest already completed"); 306 | } 307 | this.status = response.status; 308 | this.statusText = response.statusText || ""; 309 | this.readyState = FakeXMLHttpRequest.DONE; 310 | this.eventBus.trigger('readystatechange'); 311 | this.eventBus.trigger('progress'); 312 | this.eventBus.trigger('error'); 313 | this.eventBus.trigger('loadend'); 314 | }, 315 | 316 | startStream: function(options) { 317 | if (!options) { 318 | options = {}; 319 | } 320 | 321 | if (this.readyState >= FakeXMLHttpRequest.LOADING) { 322 | throw new Error("FakeXMLHttpRequest already loading or finished"); 323 | } 324 | 325 | this.status = 200; 326 | this.responseText = ""; 327 | this.statusText = ""; 328 | 329 | this.responseHeaders = normalizeHeaders(options.responseHeaders, options.contentType); 330 | this.readyState = FakeXMLHttpRequest.HEADERS_RECEIVED; 331 | this.eventBus.trigger('readystatechange'); 332 | 333 | this.responseType = options.responseType || ""; 334 | this.responseURL = options.responseURL || null; 335 | this.readyState = FakeXMLHttpRequest.LOADING; 336 | this.eventBus.trigger('readystatechange'); 337 | }, 338 | 339 | streamData: function(data) { 340 | if (this.readyState !== FakeXMLHttpRequest.LOADING) { 341 | throw new Error("FakeXMLHttpRequest is not loading yet"); 342 | } 343 | 344 | this.responseText += data; 345 | this.responseXML = getResponseXml(this.responseText, this.getResponseHeader('content-type') || ''); 346 | if (this.responseXML) { 347 | this.responseType = 'document'; 348 | } 349 | 350 | this.response = this.responseValue(); 351 | 352 | this.eventBus.trigger('readystatechange'); 353 | this.eventBus.trigger('progress'); 354 | }, 355 | 356 | cancelStream: function () { 357 | if (this.readyState === FakeXMLHttpRequest.DONE) { 358 | throw new Error("FakeXMLHttpRequest already completed"); 359 | } 360 | 361 | this.status = 0; 362 | this.statusText = ""; 363 | this.readyState = FakeXMLHttpRequest.DONE; 364 | this.eventBus.trigger('readystatechange'); 365 | this.eventBus.trigger('progress'); 366 | this.eventBus.trigger('loadend'); 367 | }, 368 | 369 | completeStream: function(status) { 370 | if (this.readyState === FakeXMLHttpRequest.DONE) { 371 | throw new Error("FakeXMLHttpRequest already completed"); 372 | } 373 | 374 | this.status = status || 200; 375 | this.statusText = ""; 376 | this.readyState = FakeXMLHttpRequest.DONE; 377 | this.eventBus.trigger('readystatechange'); 378 | this.eventBus.trigger('progress'); 379 | this.eventBus.trigger('loadend'); 380 | } 381 | }); 382 | 383 | return FakeXMLHttpRequest; 384 | } 385 | 386 | return fakeRequest; 387 | }; 388 | -------------------------------------------------------------------------------- /src/mockAjax.js: -------------------------------------------------------------------------------- 1 | getJasmineRequireObj().MockAjax = function($ajax) { 2 | function MockAjax(global) { 3 | var requestTracker = new $ajax.RequestTracker(), 4 | stubTracker = new $ajax.StubTracker(), 5 | paramParser = new $ajax.ParamParser(), 6 | realAjaxFunction = global.XMLHttpRequest, 7 | mockAjaxFunction = $ajax.fakeRequest(global, requestTracker, stubTracker, paramParser); 8 | 9 | this.install = function() { 10 | if (global.XMLHttpRequest !== realAjaxFunction) { 11 | throw new Error("Jasmine Ajax was unable to install over a custom XMLHttpRequest. Is Jasmine Ajax already installed?"); 12 | } 13 | 14 | global.XMLHttpRequest = mockAjaxFunction; 15 | }; 16 | 17 | this.uninstall = function() { 18 | if (global.XMLHttpRequest !== mockAjaxFunction) { 19 | throw new Error("MockAjax not installed."); 20 | } 21 | global.XMLHttpRequest = realAjaxFunction; 22 | 23 | this.stubs.reset(); 24 | this.requests.reset(); 25 | paramParser.reset(); 26 | }; 27 | 28 | this.stubRequest = function(url, data, method) { 29 | var stub = new $ajax.RequestStub(url, data, method); 30 | stubTracker.addStub(stub); 31 | return stub; 32 | }; 33 | 34 | this.withMock = function(closure) { 35 | this.install(); 36 | try { 37 | closure(); 38 | } finally { 39 | this.uninstall(); 40 | } 41 | }; 42 | 43 | this.addCustomParamParser = function(parser) { 44 | paramParser.add(parser); 45 | }; 46 | 47 | this.requests = requestTracker; 48 | this.stubs = stubTracker; 49 | } 50 | 51 | return MockAjax; 52 | }; 53 | -------------------------------------------------------------------------------- /src/paramParser.js: -------------------------------------------------------------------------------- 1 | getJasmineRequireObj().AjaxParamParser = function() { 2 | function ParamParser() { 3 | var defaults = [ 4 | { 5 | test: function(xhr) { 6 | return (/^application\/json/).test(xhr.contentType()); 7 | }, 8 | parse: function jsonParser(paramString) { 9 | return JSON.parse(paramString); 10 | } 11 | }, 12 | { 13 | test: function(xhr) { 14 | return true; 15 | }, 16 | parse: function naiveParser(paramString) { 17 | var data = {}; 18 | var params = paramString.split('&'); 19 | 20 | for (var i = 0; i < params.length; ++i) { 21 | var kv = params[i].replace(/\+/g, ' ').split('='); 22 | var key = decodeURIComponent(kv[0]); 23 | data[key] = data[key] || []; 24 | data[key].push(decodeURIComponent(kv[1])); 25 | } 26 | return data; 27 | } 28 | } 29 | ]; 30 | var paramParsers = []; 31 | 32 | this.add = function(parser) { 33 | paramParsers.unshift(parser); 34 | }; 35 | 36 | this.findParser = function(xhr) { 37 | for(var i in paramParsers) { 38 | var parser = paramParsers[i]; 39 | if (parser.test(xhr)) { 40 | return parser; 41 | } 42 | } 43 | }; 44 | 45 | this.reset = function() { 46 | paramParsers = []; 47 | for(var i in defaults) { 48 | paramParsers.push(defaults[i]); 49 | } 50 | }; 51 | 52 | this.reset(); 53 | } 54 | 55 | return ParamParser; 56 | }; 57 | -------------------------------------------------------------------------------- /src/requestStub.js: -------------------------------------------------------------------------------- 1 | getJasmineRequireObj().AjaxRequestStub = function() { 2 | var RETURN = 0, 3 | ERROR = 1, 4 | TIMEOUT = 2, 5 | CALL = 3; 6 | 7 | var normalizeQuery = function(query) { 8 | return query ? query.split('&').sort().join('&') : undefined; 9 | }; 10 | 11 | var timeoutRequest = function(request) { 12 | request.responseTimeout(); 13 | }; 14 | 15 | function RequestStub(url, stubData, method) { 16 | if (url instanceof RegExp) { 17 | this.url = url; 18 | this.query = undefined; 19 | } else { 20 | var split = url.split('?'); 21 | this.url = split[0]; 22 | this.query = split.length > 1 ? normalizeQuery(split[1]) : undefined; 23 | } 24 | 25 | this.data = (stubData instanceof RegExp) ? stubData : normalizeQuery(stubData); 26 | this.method = method; 27 | } 28 | 29 | RequestStub.prototype = { 30 | andReturn: function(options) { 31 | options.status = (typeof options.status !== 'undefined') ? options.status : 200; 32 | this.handleRequest = function(request) { 33 | request.respondWith(options); 34 | }; 35 | }, 36 | 37 | andError: function(options) { 38 | if (!options) { 39 | options = {}; 40 | } 41 | options.status = options.status || 500; 42 | this.handleRequest = function(request) { 43 | request.responseError(options); 44 | }; 45 | }, 46 | 47 | andTimeout: function() { 48 | this.handleRequest = timeoutRequest; 49 | }, 50 | 51 | andCallFunction: function(functionToCall) { 52 | this.handleRequest = function(request) { 53 | functionToCall(request); 54 | }; 55 | }, 56 | 57 | matches: function(fullUrl, data, method) { 58 | var urlMatches = false; 59 | fullUrl = fullUrl.toString(); 60 | if (this.url instanceof RegExp) { 61 | urlMatches = this.url.test(fullUrl); 62 | } else { 63 | var urlSplit = fullUrl.split('?'), 64 | url = urlSplit[0], 65 | query = urlSplit[1]; 66 | urlMatches = this.url === url && this.query === normalizeQuery(query); 67 | } 68 | var dataMatches = false; 69 | if (this.data instanceof RegExp) { 70 | dataMatches = this.data.test(data); 71 | } else { 72 | dataMatches = !this.data || this.data === normalizeQuery(data); 73 | } 74 | return urlMatches && dataMatches && (!this.method || this.method === method); 75 | } 76 | }; 77 | 78 | return RequestStub; 79 | }; 80 | -------------------------------------------------------------------------------- /src/requestTracker.js: -------------------------------------------------------------------------------- 1 | getJasmineRequireObj().AjaxRequestTracker = function() { 2 | function RequestTracker() { 3 | var requests = []; 4 | 5 | this.track = function(request) { 6 | requests.push(request); 7 | }; 8 | 9 | this.first = function() { 10 | return requests[0]; 11 | }; 12 | 13 | this.count = function() { 14 | return requests.length; 15 | }; 16 | 17 | this.reset = function() { 18 | requests = []; 19 | }; 20 | 21 | this.mostRecent = function() { 22 | return requests[requests.length - 1]; 23 | }; 24 | 25 | this.at = function(index) { 26 | return requests[index]; 27 | }; 28 | 29 | this.filter = function(url_to_match) { 30 | var matching_requests = []; 31 | 32 | for (var i = 0; i < requests.length; i++) { 33 | if (url_to_match instanceof RegExp && 34 | url_to_match.test(requests[i].url)) { 35 | matching_requests.push(requests[i]); 36 | } else if (url_to_match instanceof Function && 37 | url_to_match(requests[i])) { 38 | matching_requests.push(requests[i]); 39 | } else { 40 | if (requests[i].url === url_to_match) { 41 | matching_requests.push(requests[i]); 42 | } 43 | } 44 | } 45 | 46 | return matching_requests; 47 | }; 48 | } 49 | 50 | return RequestTracker; 51 | }; 52 | -------------------------------------------------------------------------------- /src/requireAjax.js: -------------------------------------------------------------------------------- 1 | getJasmineRequireObj().ajax = function(jRequire) { 2 | var $ajax = {}; 3 | 4 | $ajax.RequestStub = jRequire.AjaxRequestStub(); 5 | $ajax.RequestTracker = jRequire.AjaxRequestTracker(); 6 | $ajax.StubTracker = jRequire.AjaxStubTracker(); 7 | $ajax.ParamParser = jRequire.AjaxParamParser(); 8 | $ajax.event = jRequire.AjaxEvent(); 9 | $ajax.eventBus = jRequire.AjaxEventBus($ajax.event); 10 | $ajax.fakeRequest = jRequire.AjaxFakeRequest($ajax.eventBus); 11 | $ajax.MockAjax = jRequire.MockAjax($ajax); 12 | 13 | return $ajax.MockAjax; 14 | }; 15 | -------------------------------------------------------------------------------- /src/stubTracker.js: -------------------------------------------------------------------------------- 1 | getJasmineRequireObj().AjaxStubTracker = function() { 2 | function StubTracker() { 3 | var stubs = []; 4 | 5 | this.addStub = function(stub) { 6 | stubs.push(stub); 7 | }; 8 | 9 | this.reset = function() { 10 | stubs = []; 11 | }; 12 | 13 | this.findStub = function(url, data, method) { 14 | for (var i = stubs.length - 1; i >= 0; i--) { 15 | var stub = stubs[i]; 16 | if (stub.matches(url, data, method)) { 17 | return stub; 18 | } 19 | } 20 | }; 21 | } 22 | 23 | return StubTracker; 24 | }; 25 | --------------------------------------------------------------------------------