├── .bowerrc ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jscsrc ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── app ├── index.html ├── mock_data │ └── people │ │ └── pikachu.get.json └── scripts │ └── angular-apimock.js ├── bower.json ├── dist ├── angular-apimock.js └── angular-apimock.min.js ├── karma-e2e.conf.js ├── karma.conf.js ├── nuget ├── Angular-ApiMock.0.1.6.nupkg ├── Angular-ApiMock.0.1.7.nupkg ├── Angular-ApiMock.0.1.8.nupkg ├── Angular-ApiMock.0.2.0.nupkg ├── Angular-ApiMock.0.2.1.nupkg ├── Angular-ApiMock.0.3.0.nupkg ├── Angular-ApiMock.0.3.1.nupkg ├── Angular-ApiMock.0.3.2.nupkg └── Angular-ApiMock.0.3.3.nupkg ├── package.json ├── package.nuspec └── test ├── .jshintrc ├── ref ├── angular-mocks-v1.2.js ├── angular-mocks-v1.3.js ├── angular-mocks-v1.4.js ├── angular-mocks-v1.5.js ├── angular-v1.2.js ├── angular-v1.3.js ├── angular-v1.4.js └── angular-v1.5.js └── spec └── services └── angular-apimock.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = tab 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | .tmp 4 | .sass-cache 5 | app/bower_components 6 | .idea 7 | .grunt 8 | coverage/ 9 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "disallowKeywords": ["with"], 3 | "disallowKeywordsOnNewLine": ["else"], 4 | "disallowMixedSpacesAndTabs": true, 5 | "disallowMultipleLineStrings": true, 6 | "disallowNewlineBeforeBlockStatements": true, 7 | "disallowSpaceAfterObjectKeys": true, 8 | "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], 9 | "disallowSpaceBeforeBinaryOperators": [","], 10 | "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], 11 | "requireSpacesInAnonymousFunctionExpression": { 12 | "beforeOpeningRoundBrace": true 13 | }, 14 | "disallowSpacesInCallExpression": true, 15 | "disallowSpacesInFunctionDeclaration": { 16 | "beforeOpeningRoundBrace": true 17 | }, 18 | "disallowSpacesInNamedFunctionExpression": { 19 | "beforeOpeningRoundBrace": true 20 | }, 21 | "requireSpacesInsideArrayBrackets": { 22 | "allExcept": [ "[", "]", "{", "}" ] 23 | }, 24 | "requireSpaceBeforeKeywords": [ 25 | "else", 26 | "while", 27 | "catch" 28 | ], 29 | "disallowSpacesInsideParentheses": true, 30 | "disallowTrailingComma": true, 31 | "disallowTrailingWhitespace": true, 32 | "requireCommaBeforeLineBreak": true, 33 | "requireLineFeedAtFileEnd": true, 34 | "requireSpaceAfterBinaryOperators": ["?", ":", "+", "-", "/", "*", "%", "==", "===", "!=", "!==", ">", ">=", "<", "<=", "&&", "||"], 35 | "requireSpaceBeforeBinaryOperators": ["?", ":", "+", "-", "/", "*", "%", "==", "===", "!=", "!==", ">", ">=", "<", "<=", "&&", "||"], 36 | "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"], 37 | "requireSpaceBeforeBlockStatements": true, 38 | "requireSpacesInConditionalExpression": { 39 | "afterTest": true, 40 | "beforeConsequent": true, 41 | "afterConsequent": true, 42 | "beforeAlternate": true 43 | }, 44 | "requireSpacesInForStatement": true, 45 | "requireSpacesInFunction": { 46 | "beforeOpeningCurlyBrace": true 47 | }, 48 | "validateLineBreaks": "LF" 49 | } 50 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": false, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "angular": false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '0.12' 5 | - 'node' 6 | 7 | cache: 8 | directories: 9 | - node_modules 10 | - app/bower_components 11 | 12 | before_install: 13 | - 'npm install -g bower grunt-cli' 14 | - 'bower install' 15 | before_script: 16 | - mkdir -p $LOGS_DIR 17 | - 'export DISPLAY=:99.0' 18 | - 'sh -e /etc/init.d/xvfb start' 19 | script: 20 | - 'npm test' 21 | - 'npm run coveralls' 22 | - '[ "${TRAVIS_PULL_REQUEST}" != "false" ] || npm run saucelabs' 23 | after_script: 24 | - 'cat $LOGS_DIR/karma.log' 25 | 26 | env: 27 | global: 28 | - LOGS_DIR=/tmp/logs 29 | - secure: L+EPka9O38GaLwg3xqr6p9rwvwy4MnQH0Fba8Qlt1M5QzLRz3e9JdVvxCQrQlETR5Guc5CN1dLOQFhVyYoCFNcOs3VkeD/mnegq16LwFE9vcmxySRqleUja3eCQ4HQJP99pSiCHYQVGvoZUf82nYNz6bh7JusbGapyyUF4Nl1TU= 30 | - secure: l7fKnvanXbaLGAhlwyFRgNsSy8H5O13nbAU1AujLy9sP5tHnjHjvNLklV+wHw929h2L3pLi0OCMthDQPyA+4LnTNix6UW2f7p1tOwAxOqEWUSitWHway1pNOR9CZbpGVfdcoML/RGNgt1iwtEC9TyhxhbFw1pGZj8GLfCOVdthk= 31 | 32 | addons: 33 | sauce_connect: true 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [0.3.3](https://github.com/seriema/angular-apimock/compare/0.3.2...v0.3.3) (2015-11-14) 3 | 4 | 5 | ### Bug Fixes 6 | 7 | * **npm:** `npm install` was failing because it ran bower as `postinstall` ([a696e14](https://github.com/seriema/angular-apimock/commit/a696e14)) 8 | 9 | 10 | 11 | 12 | ## [0.3.2](https://github.com/seriema/angular-apimock/compare/0.3.1...v0.3.2) (2015-10-25) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * **apimock:** timestamps had an invalid format ([024c8de](https://github.com/seriema/angular-apimock/commit/024c8de)), closes [#57](https://github.com/seriema/angular-apimock/issues/57) 18 | 19 | 20 | 21 | 22 | ## [0.3.1](https://github.com/seriema/angular-apimock/compare/v0.3.0...v0.3.1) (2015-10-18) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * **apimock:** normalize file paths for empty query params ([9252f71](https://github.com/seriema/angular-apimock/commit/9252f71)), closes [#48](https://github.com/seriema/angular-apimock/issues/48) 28 | * **apimock:** toISOString support on dates in IE8 ([3a1ac50](https://github.com/seriema/angular-apimock/commit/3a1ac50)), closes [#51](https://github.com/seriema/angular-apimock/issues/51) 29 | 30 | 31 | 32 | 33 | # [0.3.0](https://github.com/seriema/angular-apimock/compare/v0.2.1...v0.3.0) (2015-10-05) 34 | 35 | 36 | ### Bug Fixes 37 | 38 | * **grunt:** revert failed merge ([7df9420](https://github.com/seriema/angular-apimock/commit/7df9420)) 39 | 40 | ### Features 41 | 42 | * **apiMock:** default mock setting ([f4e2258](https://github.com/seriema/angular-apimock/commit/f4e2258)), closes [#24](https://github.com/seriema/angular-apimock/issues/24) 43 | * **apiPath:** add support for arrays and/or regexp when matching ([0a08a7d](https://github.com/seriema/angular-apimock/commit/0a08a7d)), closes [#16](https://github.com/seriema/angular-apimock/issues/16) 44 | * **grunt:** enforce code style with JSCS ([583b65a](https://github.com/seriema/angular-apimock/commit/583b65a)), closes [#30](https://github.com/seriema/angular-apimock/issues/30) 45 | 46 | 47 | 48 | 49 | ## [0.2.1](https://github.com/seriema/angular-apimock/compare/v0.2.0...v0.2.1) (2015-09-13) 50 | 51 | 52 | ### Bug Fixes 53 | 54 | * **apimock:** correctly detect URL commands ([29874c9](https://github.com/seriema/angular-apimock/commit/29874c9)), closes [#37](https://github.com/seriema/angular-apimock/issues/37) 55 | * **apimock:** correctly detect URL commands ([ccf22f2](https://github.com/seriema/angular-apimock/commit/ccf22f2)), closes [#37](https://github.com/seriema/angular-apimock/issues/37) 56 | * **travis:** don't run SauceLabs on PR's ([b20c786](https://github.com/seriema/angular-apimock/commit/b20c786)), closes [#31](https://github.com/seriema/angular-apimock/issues/31) 57 | * **travis:** don't run SauceLabs tests on PR's ([0e07eb3](https://github.com/seriema/angular-apimock/commit/0e07eb3)), closes [#32](https://github.com/seriema/angular-apimock/issues/32) [#31](https://github.com/seriema/angular-apimock/issues/31) 58 | 59 | ### Features 60 | 61 | * **npm:** Angular ApiMock now available on the npm registry! ([f7d5663](https://github.com/seriema/angular-apimock/commit/f7d5663)) 62 | 63 | 64 | 65 | 66 | # 0.2.0 (2015-08-09) 67 | 68 | 69 | ### Bug Fixes 70 | 71 | * **demo:** simple demo was outdated ([96b0196](https://github.com/seriema/angular-apimock/commit/96b0196)) 72 | * **grunt:** grunt-conventional-changelog 2.0 changed name ([b21b909](https://github.com/seriema/angular-apimock/commit/b21b909)) 73 | * **tests:** switch back to Jasmine for IE8 support ([6338b69](https://github.com/seriema/angular-apimock/commit/6338b69)) 74 | 75 | ### Features 76 | 77 | * **apimock:** add network latency simulation ([2783f10](https://github.com/seriema/angular-apimock/commit/2783f10)), closes [#20](https://github.com/seriema/angular-apimock/issues/20) 78 | * **delay:** add network latency simulation ([8b996d3](https://github.com/seriema/angular-apimock/commit/8b996d3)), closes [#20](https://github.com/seriema/angular-apimock/issues/20) 79 | * **queryParams:** add query param functionality ([1e779c3](https://github.com/seriema/angular-apimock/commit/1e779c3)), closes [#23](https://github.com/seriema/angular-apimock/issues/23) 80 | * **queryParams:** ignore nested objects and arrays on $http config.param ([0e3138a](https://github.com/seriema/angular-apimock/commit/0e3138a)) 81 | * **queryParams:** lowercase query params ([2aeb262](https://github.com/seriema/angular-apimock/commit/2aeb262)) 82 | * **queryParams:** support nested objects and arrays in $http config ([4147b33](https://github.com/seriema/angular-apimock/commit/4147b33)) 83 | * **readme:** add coverage status badge ([61dc226](https://github.com/seriema/angular-apimock/commit/61dc226)) 84 | * **travis:** add Sauce Labs testing to CI ([3375513](https://github.com/seriema/angular-apimock/commit/3375513)) 85 | 86 | 87 | 88 | 89 | ### 0.1.8 (2015-03-22) 90 | 91 | 92 | #### Bug Fixes 93 | 94 | * **apimock:** Adjust for PR19. Always use 'GET' requests. ([0ae1ba57](http://github.com/seriema/angular-apimock/commit/0ae1ba571359f80a30a04f05c6a18b620932668e)) 95 | 96 | 97 | #### Features 98 | 99 | * **apimock:** update Angular dependencies in Bower to latest (below 2.x). ([8847c192](http://github.com/seriema/angular-apimock/commit/8847c192e50ed82576e6ae0e736c547ebdb1def8)) 100 | 101 | 102 | 103 | ### 0.1.7 (2014-10-19) 104 | 105 | 106 | #### Features 107 | 108 | * **apimock:** add disable config (#15) ([9e7b14a1](http://github.com/seriema/angular-apimock/commit/9e7b14a1d893a321835aa2d453a4aab5b60c01e5)) 109 | 110 | 111 | 112 | ### 0.1.6 (2014-05-29) 113 | 114 | 115 | #### Bug Fixes 116 | 117 | * **apimock:** 118 | * fix command 'auto' ([acfc2371](http://github.com/seriema/angular-apimock/commit/acfc2371079be8f428a02e31ece05e1d90bb5c38)) 119 | * treat NaN as false for command ([a28544d4](http://github.com/seriema/angular-apimock/commit/a28544d43c5d11f65095b6950fba75bd07553578)) 120 | * **nuget:** fix nuget push task ([e77e6e7f](http://github.com/seriema/angular-apimock/commit/e77e6e7f96a8da6510390b3e70ca49b0ab4d4a6a)) 121 | 122 | 123 | #### Features 124 | 125 | * **apimock:** add logging through $log ([9d93551f](http://github.com/seriema/angular-apimock/commit/9d93551f3801483a2cd479c972a89a033e88fcab)) 126 | 127 | 128 | 129 | ### 0.1.5 (2014-05-19) 130 | 131 | 132 | #### Bug Fixes 133 | 134 | * **apimock:** fix logic in automatic fallbacks ([96c9f4d5](http://github.com/seriema/angular-apimock/commit/96c9f4d578c879807dbdcbb6f3652481d1db8675)) 135 | 136 | 137 | #### Features 138 | 139 | * **apimock:** automatic fallbacks ([9245bc8a](http://github.com/seriema/angular-apimock/commit/9245bc8a7d477af87f468cb5b6b7a4397597b31f)) 140 | 141 | 142 | #### Breaking Changes 143 | 144 | * Interface for apiMock service has changed. 145 | 146 | You shouldn’t be using apiMock object anymore. There’s currently not a 147 | good way to detect if mocking is enabled as that can change on 148 | individual http-requests anyway. 149 | 150 | We’re considering printing to $log to notify when mocking is occuring. 151 | Suggestions are welcome. 152 | ([c96f9188](http://github.com/seriema/angular-apimock/commit/c96f91883ec0faef1df34e7f151a76acbed553a0)) 153 | 154 | 155 | 156 | ### 0.1.4 (2014-05-11) 157 | 158 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ApiMock started as a concept at a large eCommerce project years ago. We'd love to know how others use it and what we can do to improve it. 4 | 5 | Below we've gathered some guidelines on how to report bugs, request features, or submit pull-requests. 6 | 7 | 8 | ## Bugs 9 | 10 | Any bug reports are welcome. Check [issues](https://github.com/seriema/angular-apimock/issues/) if it is already logged or fixed before creating a new issue. 11 | 12 | Focus on a [reduced test case](https://css-tricks.com/reduced-test-cases/) and do any or all of these: 13 | - Create a [Plunker](http://plnkr.co) sample (use the `$http` config syntax for `apiMock`) 14 | - Create a [unit test](test/spec/) 15 | - Write reproduction steps and expected result compared to actual result 16 | 17 | 18 | ## Features 19 | 20 | Feature requests are more than welcome. Check [issues](https://github.com/seriema/angular-apimock/issues/) to find or add an issue where we can discuss the feature before any code is written. Consider impacts on current users. 21 | 22 | Do any or all of these: 23 | - Demonstrate usage with real or pseudo code (think [dreamcode](http://nobackend.org/dreamcode.html)) 24 | - Create a [unit test](test/spec) 25 | - Write a clear use case for when this feature would be useful for others 26 | - Create a demo for the [website](http://johansson.jp/angular-apimock/#/) 27 | - Write documentation for the [README](README.md) 28 | 29 | 30 | ## PRs (pull-requests) 31 | 32 | Please follow these guidelines for PRs. Make sure you're familiar with [GitHub PRs](https://help.github.com/articles/using-pull-requests) and issues. PRs for [the wishlist in the README](README.md#wishlist) would be especially awesome! 33 | 34 | Helpful steps: 35 | 36 | 1. One PR per logged bug or feature (read above about **Bugs** or **Features**) 37 | 2. Check for an existing issue or create a new one (see step 1) 38 | 3. Fork the project and create a new branch to do your changes in 39 | 4. Install dependencies with `npm install` and `bower install` (you'll need [Node](https://nodejs.org), [Grunt](http://gruntjs.com), and [Bower](http://bower.io) already installed) 40 | 5. Run `grunt` before every commit and see the **Commit guidelines** section below 41 | 6. Create a PR and reference the issue from step 2 42 | 43 | Never run `grunt publish`! Just use `grunt` or `grunt test` to verify that your code is working. 44 | 45 | Don't change the [CHANGELOG](CHANGELOG.md) file. It's auto-generated when publishing a new version of ApiMock. 46 | 47 | ### Code guidelines 48 | 49 | Follow the conventions (indentation, newlines, etc) in [.editorconfig](.editorconfig). Most editors have a plugin for [EditorConfig](http://editorconfig.org) that helps you. 50 | 51 | See [.jshintrc](.jshintrc) for the [JSHint](http://jshint.com) conventions (use semicolon, single quote marks, etc). Run `grunt jshint` to check that your code fulfills those settings. Otherwise just follow the conventions you see in the existing code. 52 | 53 | ### Commit guidelines 54 | 55 | Run `grunt watch` to run unit-tests and JSHint continuously while developing. Always run `grunt test` before doing a commit. *Never commit if `grunt test` gives an error!* 56 | 57 | Run `npm run debug` to debug in Chrome. 58 | 59 | 60 | Don't commit unnecessary changes (like white-space differences). Github for [Windows](https://windows.github.com) / [Mac](https://mac.github.com) has a useful diff-tool that allows you to pick files and which rows to include in the commit. 61 | 62 | #### Commit messages 63 | 64 | We use a [conventional changelog tool](https://github.com/btford/grunt-conventional-changelog) to generate the [CHANGELOG](CHANGELOG.md) so commit message formatting is really important. Check our [commit history](https://github.com/seriema/angular-apimock/commits/master) to see how it's used. 65 | 66 | ##### Summary message 67 | 68 | The first line of the commit message must be the _type_, _area_, and _summary_. As in `type(area): summary`. 69 | 70 | Example: `feat(apimock): add disable config` 71 | 72 | _Type_ must be one of the following: 73 | 74 | * **feat**: A new feature 75 | * **fix**: A bug fix 76 | * **docs**: Documentation only changes 77 | * **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 78 | * **refactor**: A code change that neither fixes a bug or adds a feature 79 | * **perf**: A code change that improves performance 80 | * **test**: Adding missing tests 81 | * **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation 82 | 83 | _Area_ should be one of these: 84 | 85 | * apimock 86 | * all 87 | * grunt 88 | * travis 89 | * readme 90 | * npm 91 | * tests (plural! don't forget the ending "s") 92 | * bower 93 | 94 | _Summary_ should preferably be less than 80 characters and succinctly summarize the commit. Use the description message (see below) to detail further and reference Github issues. 95 | 96 | ##### Description message 97 | 98 | References to issues or pull requests go after the summary message, each one on their own line. Use: 99 | 100 | * **Fixes**: When the commit fixes an open issue 101 | * **Closes**: When the commit closes an open pull request 102 | * **Ref**: When referencing an issue or pull request that is already closed or should remain open. Examples include partial fixes and commits that add a test but not a fix 103 | 104 | Example: `Fixes #15` 105 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Generated on 2014-02-20 using generator-angular 0.7.1 2 | 'use strict'; 3 | 4 | // # Globbing 5 | // for performance reasons we're only matching one level down: 6 | // 'test/spec/{,*/}*.js' 7 | // use this if you want to recursively match all subfolders: 8 | // 'test/spec/**/*.js' 9 | 10 | module.exports = function (grunt) { 11 | 12 | // Load grunt tasks automatically 13 | require('load-grunt-tasks')(grunt); 14 | 15 | // Time how long tasks take. Can help when optimizing build times 16 | require('time-grunt')(grunt); 17 | 18 | // Define the configuration for all the tasks 19 | grunt.initConfig({ 20 | 21 | // Project settings 22 | yeoman: { 23 | // configurable paths 24 | version: require('./package.json').version, 25 | app: 'app', 26 | dist: 'dist', 27 | test: 'test' 28 | }, 29 | 30 | // Test run with `grunt bump --dry-run` 31 | bump: { 32 | options: { 33 | files: [ 'package.json', 'bower.json' ], 34 | updateConfigs: [ 'yeoman' ], 35 | commit: true, 36 | commitMessage: 'chore(release): release %VERSION%. See CHANGELOG.md', 37 | commitFiles: [ '-a' ], // '-a' for all files 38 | createTag: true, 39 | tagName: '%VERSION%', 40 | tagMessage: 'Version %VERSION%', 41 | push: true, 42 | pushTo: 'origin', 43 | gitDescribeOptions: '--tags --always --abbrev=1 --dirty=-d' // options to use with '$ git describe' 44 | } 45 | }, 46 | 47 | conventionalChangelog: { 48 | options: { 49 | changelogOpts: { 50 | preset: 'angular' 51 | } 52 | }, 53 | release: { 54 | src: 'CHANGELOG.md' 55 | } 56 | }, 57 | 58 | conventionalGithubReleaser: { 59 | release: { 60 | options: { 61 | auth: { 62 | type: 'oauth', 63 | token: process.env.GITHUB_PERSONAL_ACCESS_TOKEN 64 | }, 65 | changelogOpts: { 66 | preset: 'angular' 67 | } 68 | } 69 | } 70 | }, 71 | 72 | // Watches files for changes and runs tasks based on the changed files 73 | watch: { 74 | js: { 75 | files: [ '<%= yeoman.app %>/scripts/{,*/}*.js' ], 76 | tasks: [ 'newer:jshint:all', 'karma:default' ], 77 | options: { 78 | livereload: true 79 | } 80 | }, 81 | jsTest: { 82 | files: [ 'test/spec/{,*/}*.js' ], 83 | tasks: [ 'newer:jshint:test', 'karma:default' ] 84 | }, 85 | gruntfile: { 86 | files: [ 'Gruntfile.js' ] 87 | }, 88 | livereload: { 89 | options: { 90 | livereload: '<%= connect.options.livereload %>' 91 | }, 92 | files: [ 93 | '<%= yeoman.app %>/{,*/}*.html' 94 | ] 95 | } 96 | }, 97 | 98 | // The actual grunt server settings 99 | connect: { 100 | options: { 101 | port: 0, 102 | // Change this to '0.0.0.0' to access the server from outside. 103 | hostname: 'localhost', 104 | livereload: 35729 105 | }, 106 | test: { 107 | options: { 108 | port: 0, 109 | base: [ 110 | 'test', 111 | '<%= yeoman.app %>' 112 | ] 113 | } 114 | }, 115 | livereload: { 116 | options: { 117 | open: true, 118 | base: [ 119 | '<%= yeoman.app %>' 120 | ] 121 | } 122 | } 123 | }, 124 | 125 | // Make sure code styles are up to par and there are no obvious mistakes 126 | jshint: { 127 | options: { 128 | jshintrc: '.jshintrc', 129 | reporter: require('jshint-stylish') 130 | }, 131 | all: [ 132 | 'Gruntfile.js', 133 | '<%= yeoman.app %>/scripts/{,*/}*.js' 134 | ], 135 | test: { 136 | options: { 137 | jshintrc: 'test/.jshintrc' 138 | }, 139 | src: [ 'test/spec/{,*/}*.js' ] 140 | } 141 | }, 142 | 143 | // Check code style guidelines 144 | jscs: { 145 | src: [ 146 | '<%= yeoman.app %>/scripts/**/*.js', 147 | '<%= yeoman.test %>/spec/**/*.js', 148 | '*.js' 149 | ], 150 | options: { 151 | config: '.jscsrc' 152 | } 153 | }, 154 | 155 | // Empties folders to start fresh 156 | clean: { 157 | dist: { 158 | files: [{ 159 | dot: true, 160 | src: [ 161 | '<%= yeoman.dist %>/*', 162 | '!<%= yeoman.dist %>/.git*' 163 | ] 164 | }] 165 | } 166 | }, 167 | 168 | // Allow the use of non-minsafe AngularJS files. Automatically makes it 169 | // minsafe compatible so Uglify does not destroy the ng references 170 | ngAnnotate: { 171 | options: { 172 | singleQuotes: true 173 | }, 174 | dist: { 175 | src: [ '<%= yeoman.dist %>/angular-apimock.js' ], 176 | dest: '<%= yeoman.dist %>/angular-apimock.js' 177 | } 178 | }, 179 | 180 | uglify: { 181 | options: { 182 | preserveComments: 'some', 183 | report: 'gzip' 184 | }, 185 | dist: { 186 | files: { 187 | '<%= yeoman.dist %>/angular-apimock.min.js': [ 188 | '<%= yeoman.dist %>/angular-apimock.js' 189 | ] 190 | } 191 | } 192 | }, 193 | 194 | concat: { 195 | options: { 196 | banner: '/*! Angular API Mock v<%= yeoman.version %>\n * Licensed with MIT\n * Made with ♥ from Seriema + Redhorn */\n' 197 | }, 198 | dist: { 199 | src: '<%= yeoman.app %>/scripts/**/*.js', 200 | dest: '<%= yeoman.dist %>/angular-apimock.js' 201 | } 202 | }, 203 | 204 | nugetpack: { 205 | dist: { 206 | src: 'package.nuspec', 207 | dest: 'nuget/', 208 | options: { 209 | version: '<%= yeoman.version %>' 210 | } 211 | } 212 | }, 213 | 214 | nugetpush: { 215 | dist: { 216 | src: 'nuget/Angular-ApiMock.<%= yeoman.version %>.nupkg' 217 | } 218 | }, 219 | 220 | gitadd: { 221 | task: { 222 | files: { 223 | src: [ 'nuget/Angular-ApiMock.<%= yeoman.version %>.nupkg' ] 224 | } 225 | } 226 | }, 227 | 228 | // Test settings 229 | karma: { 230 | options: { 231 | configFile: 'karma.conf.js', 232 | singleRun: true 233 | }, 234 | default: { 235 | // Default 236 | }, 237 | sauce: { 238 | browsers: [ 'SL_Chrome', 'SL_Firefox', 'SL_Safari', 'SL_iOS', 'SL_IE_8', 'SL_IE_9', 'SL_IE_10', 'SL_IE_11' ], 239 | reporters: [ 'progress', 'saucelabs' ], 240 | files: [{ 241 | src: [ 242 | // It has to be Angular 1.2 because it's the lowest one, and the only one that runs on IE8. 243 | 'test/ref/angular-v1.2.js', 244 | 'test/ref/angular-mocks-v1.2.js', 245 | '<%= watch.js.files %>', 246 | '<%= watch.jsTest.files %>' 247 | ]} 248 | ] 249 | }, 250 | coverage: { 251 | browsers: [ 'PhantomJS' ], 252 | reporters: [ 'dots', 'coverage' ], 253 | coverageReporter: { 254 | reporters: [ 255 | { type: 'lcov', subdir: 'PhantomJS' }, 256 | { type: 'text' } 257 | ] 258 | } 259 | }, 260 | dist: { 261 | files: [{ 262 | src: [ 263 | 'app/bower_components/angular/angular.js', 264 | 'app/bower_components/angular-mocks/angular-mocks.js', 265 | '<%= yeoman.dist %>/*.min.js', 266 | '<%= watch.jsTest.files %>' 267 | ]} 268 | ] 269 | }, 270 | angular12: { 271 | files: [{ 272 | src: [ 273 | 'test/ref/angular-v1.2.js', 274 | 'test/ref/angular-mocks-v1.2.js', 275 | '<%= watch.js.files %>', 276 | '<%= watch.jsTest.files %>' 277 | ]} 278 | ] 279 | }, 280 | angular13: { 281 | files: [{ 282 | src: [ 283 | 'test/ref/angular-v1.3.js', 284 | 'test/ref/angular-mocks-v1.3.js', 285 | '<%= watch.js.files %>', 286 | '<%= watch.jsTest.files %>' 287 | ]} 288 | ] 289 | }, 290 | angular14: { 291 | files: [{ 292 | src: [ 293 | 'test/ref/angular-v1.4.js', 294 | 'test/ref/angular-mocks-v1.4.js', 295 | '<%= watch.js.files %>', 296 | '<%= watch.jsTest.files %>' 297 | ]} 298 | ] 299 | }, 300 | angular15: { 301 | files: [{ 302 | src: [ 303 | 'test/ref/angular-v1.5.js', 304 | 'test/ref/angular-mocks-v1.5.js', 305 | '<%= watch.js.files %>', 306 | '<%= watch.jsTest.files %>' 307 | ]} 308 | ] 309 | } 310 | }, 311 | 312 | // To run locally you need to set `COVERALLS_REPO_TOKEN` as an environment variable. 313 | // It's currently being run from Travis-CI (see .travis.yml) 314 | coveralls: { 315 | options: { 316 | force: true 317 | }, 318 | src: 'coverage/**/*.info' 319 | } 320 | }); 321 | 322 | grunt.registerTask('serve', [ 323 | 'connect', 324 | 'watch:js' 325 | ]); 326 | 327 | grunt.registerTask('test', [ 328 | 'jshint', 329 | 'jscs', 330 | 'connect:test', 331 | 'karma:coverage', 332 | 'karma:angular12', 333 | 'karma:angular13', 334 | 'karma:angular14', 335 | 'karma:angular15' 336 | ]); 337 | 338 | grunt.registerTask('build', [ 339 | 'clean', 340 | 'concat', 341 | 'ngAnnotate', 342 | 'uglify', 343 | 'karma:dist' 344 | ]); 345 | 346 | grunt.registerTask('_publish', [ 347 | 'build', 348 | 'nugetpack', 349 | 'gitadd', 350 | 'conventionalChangelog', 351 | 'bump-commit', 352 | 'conventionalGithubReleaser', 353 | 'nugetpush', 354 | 'npm-publish' 355 | ]); 356 | 357 | grunt.registerTask('publish', [ 'publish:patch' ]); 358 | grunt.registerTask('publish:patch', [ 'validate-package', 'test', 'karma:sauce', 'bump-only:patch', '_publish' ]); 359 | grunt.registerTask('publish:minor', [ 'validate-package', 'test', 'karma:sauce', 'bump-only:minor', '_publish' ]); 360 | grunt.registerTask('publish:major', [ 'validate-package', 'test', 'karma:sauce', 'bump-only:major', '_publish' ]); 361 | 362 | grunt.registerTask('default', [ 363 | 'test' 364 | ]); 365 | }; 366 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 John-Philip Johansson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ApiMock for AngularJS: UI-first development 2 | 3 | [![Build Status](https://img.shields.io/travis/seriema/angular-apimock.svg)](https://travis-ci.org/seriema/angular-apimock) 4 | [![Coverage Status](https://img.shields.io/coveralls/seriema/angular-apimock.svg)](https://coveralls.io/github/seriema/angular-apimock?branch=master) 5 | [![devDependency Status](https://img.shields.io/david/dev/seriema/angular-apimock.svg)](https://david-dm.org/seriema/angular-apimock#info=devDependencies) 6 | 7 | [![Join the chat at https://gitter.im/seriema/angular-apimock](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/seriema/angular-apimock?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 8 | 9 | ApiMock is a minimal (1.68kb gzipped) library for AngularJS that allows you to mock your RESTful API by routing your API calls to static JSON files. 10 | 11 | 12 | ## Example 13 | The left shows the page where the API is missing. The right shows the same page, but API calls being rerouted to static JSON files. 14 | 15 | ![Online](https://dl.dropboxusercontent.com/u/5566693/Screenshot%202014-02-23%2015.04.25.png) ![Offline](https://dl.dropboxusercontent.com/u/5566693/Screenshot%202014-02-23%2015.03.54.png) 16 | 17 | 18 | ## Try it out 19 | 20 | Go to our [website demo](http://johansson.jp/angular-apimock/#/demo-simple) to try it out. That's the simplest way to understand. 21 | 22 | 23 | ## Get started 24 | 25 | Download it [here](https://raw.githubusercontent.com/seriema/angular-apimock/master/dist/angular-apimock.min.js). Or grab it through [NuGet](https://www.nuget.org/packages/Angular-ApiMock/), [Bower](http://bower.io/search/?q=angular-apimock), or [npm](https://www.npmjs.com/package/angular-apimock): 26 | 27 | * [NuGet](https://www.nuget.org/packages/Angular-ApiMock/): `PM> Install-Package Angular-ApiMock` 28 | * [Bower](http://bower.io/search/?q=angular-apimock): `$ bower install angular-apimock --save` 29 | * [npm](https://www.npmjs.com/package/angular-apimock): `$ npm install angular-apimock --save` 30 | 31 | 32 | Include `angular-apimock.min.js` in your HTML: 33 | ```` 34 | 35 | ```` 36 | 37 | Add `apiMock` as a dependency in your AngularJS app config (e.g. `app.js`): 38 | ```` 39 | angular.module('myApp', ['apiMock']) ... 40 | ```` 41 | 42 | Now use `$http` as usual. When you're looking at your webpage and want to use mock data, just add `?apimock=true` to the _browser_ page URL. This way you never need to change your JavaScript! 43 | 44 | You can also do individual overrides right in the `config` object to `$http`. E.g. `$http( { url: '/...', method: GET, apiMock: true } )`. 45 | 46 | If you want to design/test your error-handling then you can give a HTTP status code instead of `true`. So `?apimock=401` will fail all requests with status code `401` (unauthorized). This is probably more useful on individual `$http` requests. 47 | 48 | You can also set it to automatically reroute API calls that fail. Just set the parameter to `auto` (`apimock=auto` in browser or $http call). 49 | 50 | ApiMock appends the HTTP-verb before `.json` so a GET-request to `/api/customers/5` will be routed to `/mock_data/customers/5.get.json`. Now just fill your `/mock_data` directory with all the JSON files you want to grab. 51 | 52 | 53 | ## Options 54 | 55 | ### Usage 56 | 57 | ApiMock supports several operation modes. They're set globally and/or locally, where globally means on the browser URL and locally means in the $http request. There's only one parameter, `apiMock` (case-insensitive). 58 | 59 | #### apiMock 60 | 61 | Type: `boolean/string/number` 62 | 63 | Default: `undefined` 64 | 65 | Values: 66 | 67 | `true`: reroutes all requests 68 | 69 | `false`: turns off rerouting 70 | 71 | `auto`: will try the original request, if it fails then it tries to recover with a reroute 72 | 73 | `404`, `500`, etc: will reject all requests with the given HTTP status code 74 | 75 | ##### Global flag 76 | 77 | In the browser URL, just append `?apiMock=command` where `command` is described above (`true`, `auto`, etc). 78 | 79 | ##### Local flag 80 | 81 | In the JavaScript, where you do the `$http` request, the request object needs an attribute `apiMock` with the value set to the command described above (`true`, `auto`, etc). 82 | 83 | E.g. 84 | ```` 85 | $http({ method: 'GET', url: '...', apiMock: true }); 86 | ```` 87 | 88 | ### Config 89 | 90 | ApiMock follows a simple concept: reroute HTTP requests, from `apiPath` to `mockDataPath`. So you can change the paths but any deeper configuration is probably easier to write your own `httpInterceptor` (check the FAQ). 91 | 92 | Configure is done through `apiMockProvider.config()`. Add this to your AngularJS config file (e.g. `app.js`): 93 | ```` 94 | .config(function (apiMockProvider) { 95 | apiMockProvider.config({ 96 | mockDataPath: '/my_mock_data_path', 97 | apiPath: '/my_api_path', 98 | }); 99 | }); 100 | ```` 101 | 102 | #### defaultMock 103 | 104 | Type: `boolean/string/number` 105 | 106 | Default: `false` 107 | 108 | Sets a default mock value. See [apiMock values](#apimock). 109 | 110 | #### mockDataPath 111 | 112 | Type: `string` 113 | 114 | Default: `'/mock_data'` 115 | 116 | Set the path to be rerouted to. 117 | 118 | #### apiPath 119 | 120 | Type: `string` | `RegExp` | `[]` 121 | 122 | Default: `'/api'` 123 | 124 | Set the path to be rerouted from, for strings, will match request path from the left part, if it is a regular expresion it will evaluate expression. 125 | It can handle arrays and both mixed. 126 | 127 | #### disable 128 | 129 | Type: `boolean` 130 | 131 | Default: `false` 132 | 133 | Disable apiMock completely. Used for production. 134 | 135 | #### stripQueries 136 | 137 | Type: `boolean` 138 | 139 | Default: `true` 140 | 141 | Remove query strings from url. If false then "?" is replaced with "\" in expected filepath. 142 | 143 | #### delay 144 | 145 | Type: `number` 146 | 147 | Default: `0` 148 | 149 | Simulate network latency (in milliseconds). 150 | 151 | 152 | ## Samples 153 | 154 | Check the [source code](https://github.com/seriema/angular-apimock/blob/gh-pages-dev/app/scripts/controllers/demo-simple.js) for our [website demo](http://johansson.jp/angular-apimock/#/demo-simple). We're working on more demos. :) 155 | 156 | 157 | ## FAQ 158 | 159 | ### Why not just use [Interfake](https://github.com/basicallydan/interfake)? 160 | Interfake is a great complement to ApiMock. We assume you have a way to serve static JSON files. That can be because you're on a project with a server already set up and you can't do many changes to it but at least you can add static files. If you don't have that, then Interfake is a great way to set it up. Our idea is that the frontend JS doesn't change between calling the "real" API and the "fake" one. 161 | 162 | ### Why would I want to reroute my API calls? 163 | Sometimes you don't have control over the API. It could be down for some reason, or it might not have been developed yet. ApiMock allows you as a frontend developer to continue working on the UI without changing any code. It's also helpful in figuring out what your API actually _should_ have as you can play around with your static JSON and then have it serve the role as documentation for backend developers. 164 | 165 | ### Isn't this the same as `$httpBackend`? 166 | No, but it works in a similar fashion: it routes HTTP calls. Our initial implementation of apiMock used `$httpBackend` but then it would route _all_ AJAX requests and we only wanted to route API calls. A difference that's noticed when Angular tries to get HTML templates for directives, or if you try to load an image through AJAX. `$httpBackend` is for unit testing, `apiMock` is for the actual webpage. 167 | 168 | ### Is there a complete "offline" mode? 169 | Like disabling all network traffic yet things work? No, but it's a good idea. It would be perfect for presentation demo's when the WiFi is unreliable. If you have an idea of how to implement this, let us know! 170 | 171 | ### Can I mock when [...] or instead of URL replacing can I [...]? 172 | Actually the basic idea here is to intercept http calls then do something that helps at design-time of the website. This project, `angular-apimock`, aims to do that through rerouting API calls to static JSON files. We've experimented with making that flexible so you could configure it to do whatever you want, but that requires so much from this project and the core functionality (http interceptors) is so simple it's probably easier to create your own. If so, here's the basics: 173 | ```` 174 | angular.module('myModule', []) 175 | 176 | .config(function ($httpProvider) { 177 | $httpProvider.interceptors.push('yourHttpInterceptor'); 178 | }) 179 | 180 | .service('yourHttpInterceptor', function($q) { 181 | this.request = function (req) { 182 | if (req) { 183 | // Do whatever you want to the request here. 184 | } 185 | 186 | return req || $q.when(req); 187 | }; 188 | }); 189 | ```` 190 | 191 | [This blog post](http://www.webdeveasy.com/interceptors-in-angularjs-and-useful-examples/) is pretty good at diving deeper into this. 192 | 193 | ## Wishlist 194 | 195 | * Demo based on Magic The Gathering cards (reference to a //build presentation) 196 | * Demo for checking mock-flag 197 | * Demo with [Interfake](https://github.com/basicallydan/interfake) 198 | * Handle body data in POST requests? 199 | * HTTP response overrides (200?) shouldn't always go to $http.error() 200 | * Test `apimock=true` in more scenarios 201 | * Remember mock-mode after page navigation 202 | * Plunkr demos 203 | * Visual queue that mock is happening. Maybe also $log? 204 | * Work with $resource (maybe it does already?) 205 | 206 | 207 | ## Contribute 208 | 209 | ApiMock started as a concept on a large eCommerce project years ago. We'd love to get feedback or help to improve things. If you want to contribute, take a look at [CONTRIBUTING](CONTRIBUTING.md). 210 | 211 | 212 | ♥ from [Seriema](http://johansson.jp) + [Redhorn](http://redhorn.se/) 213 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular API Mock test page 6 | 7 | 8 | 9 |
10 |

11 | Name: {{name}} 12 |

13 | 14 |

Debug info

15 |

16 | $location.protocol() = {{$location.protocol()}}
17 | $location.host() = {{$location.host()}}
18 | $location.port() = {{$location.port()}}
19 | $location.path() = {{$location.path()}}
20 | $location.search() = {{$location.search()}}
21 | $location.hash() = {{$location.hash()}}
22 |

23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/mock_data/people/pikachu.get.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Pikachu" 3 | } 4 | -------------------------------------------------------------------------------- /app/scripts/angular-apimock.js: -------------------------------------------------------------------------------- 1 | /* Create the main module, `apiMock`. It's the one that needs to be included in 2 | * your projects. E.g. `angular.module('myApp', ['apiMock'])`. You don't need 3 | * to do anything else, but you can configure the paths for api-calls and mock 4 | * data by calling `app.config(function (apiMockProvider) { ... });`. 5 | */ 6 | angular.module('apiMock', []) 7 | 8 | .config(function ($httpProvider) { 9 | /* This is where the magic happens. Configure `$http` to use our 10 | `httpInterceptor` on all calls. It's what allows us to do automatic routing. 11 | */ 12 | $httpProvider.interceptors.push('httpInterceptor'); 13 | }) 14 | 15 | .provider('apiMock', function () { 16 | /* This is the Provider for apiMock. It's used by `httpInterceptor` to support 17 | * mocking. 18 | * 19 | * Config options: 20 | * `mockDataPath` string: the path to be rerouted to. Default: `/mock_data`. 21 | * `apiPath` string: the path to be rerouted from. Default: `/api`. 22 | * 23 | * Public interface: 24 | * `onRequest` method: takes a `request` object and decides if mocking should 25 | * be done on this request. It checks global and local apiMock flags to see 26 | * if it should mock. It also checks the request URL if it starts with `apiPath`. 27 | * If the request is to have a `recover` attempt it's put in the fallbacks list. 28 | * A GET request to `/api/user/5?option=full` turns into `/mock_data/user/5.get.json`. 29 | * `onResponse` method: takes a `request` object and simply removes it from list 30 | * of fallbacks for `recover`. 31 | * `recover` method: if request has been marked for recover `onRequest` then it 32 | * will reroute to mock data. This is only to be called on response error. 33 | * 34 | * Private members: 35 | * `_countFallbacks` method: returns the current number of fallbacks in queue. 36 | * Only used for unit testing. 37 | */ 38 | 39 | // Helper objects 40 | // 41 | 42 | var $location; 43 | var $log; 44 | var $q; 45 | var $filter; 46 | var config = { 47 | defaultMock: false, 48 | mockDataPath: '/mock_data', 49 | apiPath: '/api', 50 | disable: false, 51 | stripQueries: true, 52 | delay: 0 53 | }; 54 | var fallbacks = []; 55 | 56 | // Helper methods 57 | // 58 | 59 | // TODO: IE8: remove when we drop IE8/Angular 1.2 support. 60 | // Object.keys isn't supported in IE8. Which we need to support as long as we support Angular 1.2. 61 | // This isn't a complete polyfill! It's just enough for what we need (and we don't need to bloat). 62 | function objectKeys(object) { 63 | var keys = []; 64 | 65 | angular.forEach(object, function (value, key) { 66 | keys.push(key); 67 | }); 68 | 69 | return keys; 70 | } 71 | 72 | // TODO: IE8: remove when we drop IE8/Angular 1.2 support. 73 | // Date.prototype.toISOString isn't supported in IE8. Which we need to support as long as we support Angular 1.2. 74 | // Modified from MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString 75 | function toISOString(date) { 76 | function pad(number) { 77 | if (number < 10) { 78 | return '0' + number; 79 | } 80 | return number; 81 | } 82 | 83 | return date.getUTCFullYear() + 84 | '-' + pad(date.getUTCMonth() + 1) + 85 | '-' + pad(date.getUTCDate()) + 86 | 'T' + pad(date.getUTCHours()) + 87 | '.' + pad(date.getUTCMinutes()) + 88 | '.' + pad(date.getUTCSeconds()) + 89 | '.' + (date.getUTCMilliseconds() / 1000).toFixed(3).slice(2, 5) + 90 | 'Z'; 91 | } 92 | 93 | // Taken as-is from Angular 1.4.x: https://github.com/angular/angular.js/blob/f13852c179ffd9ec18b7a94df27dec39eb5f19fc/src/Angular.js#L296 94 | function forEachSorted(obj, iterator, context) { 95 | var keys = objectKeys(obj).sort(); 96 | for (var i = 0; i < keys.length; i++) { 97 | iterator.call(context, obj[keys[i]], keys[i]); 98 | } 99 | return keys; 100 | } 101 | 102 | // Modified from Angular 1.4.x: https://github.com/angular/angular.js/blob/929ec6ba5a60e926654583033a90aebe716123c0/src/ng/http.js#L18 103 | function serializeValue(v) { 104 | if (angular.isDate(v)) { 105 | return toISOString(v); 106 | } 107 | 108 | return v; 109 | } 110 | 111 | // Modified from Angular 1.4.x: https://github.com/angular/angular.js/blob/720012eab6fef5e075a1d6876dd2e508c8e95b73/src/ngResource/resource.js#L405 112 | function encodeUriQuery(val) { 113 | return encodeURIComponent(val). 114 | replace(/%40/gi, '@'). 115 | replace(/%3A/gi, ':'). 116 | replace(/%24/g, '$'). 117 | replace(/%2C/gi, ','). 118 | replace(/%20/g, '+'); 119 | } 120 | 121 | // TODO: replace with a $httpParamSerializerJQLikeProvider() call when we require Angular 1.4 (i.e. when we drop 1.2 and 1.3). 122 | // Modified from Angular 1.4.x: https://github.com/angular/angular.js/blob/929ec6ba5a60e926654583033a90aebe716123c0/src/ng/http.js#L108 123 | function jQueryLikeParamSerializer(params) { 124 | var parts = []; 125 | 126 | function serialize(toSerialize, prefix, topLevel) { 127 | if (angular.isArray(toSerialize)) { 128 | // Serialize arrays. 129 | angular.forEach(toSerialize, function (value, index) { 130 | serialize(value, prefix + '[' + (angular.isObject(value) ? index : '') + ']'); 131 | }); 132 | } else if (angular.isObject(toSerialize) && !angular.isDate(toSerialize)) { 133 | // Serialize objects (not dates, because that's covered by the default case). 134 | forEachSorted(toSerialize, function (value, key) { 135 | serialize(value, prefix + 136 | (topLevel ? '' : '[') + 137 | key + 138 | (topLevel ? '' : ']')); 139 | }); 140 | } else if (toSerialize === undefined || toSerialize === '') { 141 | // Keep empty parameters as it still affects the mock file path. 142 | parts.push(encodeUriQuery(prefix)); 143 | } else { 144 | // Serialize everything else (including dates). 145 | parts.push(encodeUriQuery(prefix) + '=' + encodeUriQuery(serializeValue(toSerialize))); 146 | } 147 | } 148 | 149 | serialize(params, '', true); 150 | return parts.join('&'); 151 | } 152 | 153 | function queryStringToObject(paramString) { 154 | if (!paramString) { 155 | return {}; 156 | } 157 | 158 | var paramArray = paramString.split('&'); 159 | 160 | var result = {}; 161 | angular.forEach(paramArray, function (param) { 162 | param = param.split('='); 163 | result[param[0]] = param[1] || ''; 164 | }); 165 | 166 | return result; 167 | } 168 | 169 | function detectParameter(keys) { 170 | var regex = /apimock/i; 171 | var result; 172 | 173 | angular.forEach(keys, function (value, key) { 174 | if (regex.test(key)) { 175 | result = value; 176 | } 177 | }); 178 | 179 | return result; 180 | } 181 | 182 | function localMock(req) { 183 | return detectParameter(req); 184 | } 185 | 186 | function globalMock() { 187 | return detectParameter($location.search()); 188 | } 189 | 190 | function getParameter(req) { 191 | var mockValue = localMock(req); 192 | // Note: `false` is a valid option, so we can't use falsy-checks. 193 | if (mockValue === undefined) { 194 | mockValue = globalMock(); 195 | } 196 | if (mockValue === undefined) { 197 | mockValue = config.defaultMock; 198 | } 199 | 200 | return mockValue; 201 | } 202 | 203 | function getCommand(mockValue) { 204 | // Depending how we got mockValue it might've been parsed into a type or not. 205 | switch ((mockValue || '').toString().toLowerCase()) { 206 | case '200': 207 | case '404': 208 | case '500': 209 | return { type: 'respond', value: parseInt(mockValue, 10) }; 210 | 211 | case 'auto': 212 | return { type: 'recover' }; 213 | 214 | case 'true': 215 | return { type: 'reroute' }; 216 | } 217 | 218 | return { type: 'ignore' }; 219 | } 220 | 221 | 222 | function httpStatusResponse(status) { 223 | var response = { 224 | status: status, 225 | headers: { 226 | 'Content-Type': 'text/html; charset=utf-8', 227 | 'Server': 'Angular ApiMock' 228 | } 229 | }; 230 | $log.info('apiMock: mocking HTTP status to ' + status); 231 | return $q.reject(response); 232 | } 233 | 234 | function apiPathMatched(url, apiPath) { 235 | var match; // Lets initially assume undefined as no match 236 | 237 | if (angular.isArray(apiPath)) { 238 | angular.forEach(apiPath, function (path) { 239 | if (match) { return; } // Hack to skip more recursive calls if already matched 240 | var found = apiPathMatched(url, path); 241 | if (found) { 242 | match = found; 243 | } 244 | }); 245 | } 246 | if (match) { 247 | return match; 248 | } 249 | if (apiPath instanceof RegExp) { 250 | if (apiPath.test(url)) { 251 | return apiPath; 252 | } 253 | } 254 | if ((url.toString().indexOf(apiPath) === 0)) { 255 | return apiPath; 256 | } 257 | return match; 258 | } 259 | 260 | function isApiPath(url) { 261 | return (apiPathMatched(url, config.apiPath) !== undefined); 262 | } 263 | 264 | function prepareFallback(req) { 265 | if (isApiPath(req.url)) { 266 | fallbacks.push(req); 267 | } 268 | } 269 | 270 | function removeFallback(res) { 271 | var startLength = fallbacks.length; 272 | fallbacks = $filter('filter')(fallbacks, { 273 | method: '!' + res.method, 274 | url: '!' + res.url 275 | }, true); 276 | 277 | return startLength > fallbacks.length; 278 | } 279 | 280 | function reroute(req) { 281 | if (!isApiPath(req.url)) { 282 | return req; 283 | } 284 | 285 | // replace apiPath with mockDataPath. 286 | var oldPath = req.url; 287 | 288 | var redirectedPath = req.url.replace(apiPathMatched(req.url, config.apiPath), config.mockDataPath); 289 | 290 | var split = redirectedPath.split('?'); 291 | var newPath = split[0]; 292 | var queries = split[1] || ''; 293 | 294 | // query strings are stripped by default (like ?search=banana). 295 | if (!config.stripQueries) { 296 | 297 | //test if we have query params 298 | //if we do merge them on to the params object 299 | var queryParamsFromUrl = queryStringToObject(queries); 300 | var params = angular.extend(req.params || {}, queryParamsFromUrl); 301 | 302 | //test if there is already a trailing / 303 | if (newPath[newPath.length - 1] !== '/') { 304 | newPath += '/'; 305 | } 306 | 307 | //serialize the param object to convert to string 308 | //and concatenate to the newPath 309 | newPath += angular.lowercase(jQueryLikeParamSerializer(params)); 310 | } 311 | 312 | //Kill the params property so they aren't added back on to the end of the url 313 | req.params = undefined; 314 | 315 | // add file endings (method verb and .json). 316 | if (newPath[newPath.length - 1] === '/') { 317 | newPath = newPath.slice(0, -1); 318 | } 319 | newPath += '.' + req.method.toLowerCase() + '.json'; 320 | 321 | req.method = 'GET'; 322 | req.url = newPath; 323 | $log.info('apiMock: rerouting ' + oldPath + ' to ' + newPath); 324 | 325 | return req; 326 | } 327 | 328 | // Expose public interface for provider instance 329 | // 330 | 331 | function ApiMock(_$location, _$log, _$q, _$filter) { 332 | $location = _$location; 333 | $log = _$log; 334 | $q = _$q; 335 | $filter = _$filter; 336 | } 337 | 338 | var p = ApiMock.prototype; 339 | 340 | p._countFallbacks = function () { 341 | return fallbacks.length; 342 | }; 343 | 344 | p.getDelay = function () { 345 | return config.delay; 346 | }; 347 | 348 | p.onRequest = function (req) { 349 | if (config.disable) { 350 | return req; 351 | } 352 | 353 | var param = getParameter(req); 354 | var command = getCommand(param); 355 | 356 | switch (command.type) { 357 | case 'reroute': 358 | return reroute(req); 359 | case 'recover': 360 | prepareFallback(req); 361 | return req; 362 | case 'respond': 363 | return httpStatusResponse(command.value); 364 | case 'ignore': 365 | /* falls through */ 366 | default: 367 | return req; 368 | } 369 | }; 370 | 371 | p.onResponse = function (res) { 372 | removeFallback(res); 373 | return res; 374 | }; 375 | 376 | p.recover = function (rej) { 377 | if (config.disable) { 378 | return false; 379 | } 380 | 381 | if (rej.config === undefined) {// Why is this called with regular response object sometimes? 382 | return false; 383 | } 384 | 385 | if (removeFallback(rej.config)) { 386 | $log.info('apiMock: recovering from failure at ' + rej.config.url); 387 | return reroute(rej.config); 388 | } 389 | 390 | return false; 391 | }; 392 | 393 | // Expose Provider interface 394 | // 395 | 396 | this.config = function (options) { 397 | angular.extend(config, options); 398 | }; 399 | 400 | this.$get = function ($location, $log, $q, $filter) { 401 | return new ApiMock($location, $log, $q, $filter); 402 | }; 403 | }) 404 | 405 | .service('httpInterceptor', function ($injector, $q, $timeout, apiMock) { 406 | /* The main service. Is jacked in as a interceptor on `$http` so it gets called 407 | * on every http call. This allows us to do our magic. It uses the provider 408 | * `apiMock` to determine if a mock should be done, then do the actual mocking. 409 | */ 410 | this.request = function (req) { 411 | return apiMock.onRequest(req); 412 | }; 413 | 414 | this.response = function (res) { 415 | var deferred = $q.defer(); 416 | 417 | $timeout( 418 | function () { 419 | // TODO: Apparently, no tests break regardless what this resolves to. Fix the tests! 420 | deferred.resolve(apiMock.onResponse(res)); 421 | }, 422 | apiMock.getDelay(), 423 | true // Trigger a $digest. 424 | ); 425 | 426 | return deferred.promise; 427 | }; 428 | 429 | this.responseError = function (rej) { 430 | var deferred = $q.defer(); 431 | 432 | $timeout( 433 | function () { 434 | var recover = apiMock.recover(rej); 435 | 436 | if (recover) { 437 | var $http = $injector.get('$http'); 438 | $http(recover).then(function (data) { 439 | deferred.resolve(data); 440 | }); 441 | } else { 442 | deferred.reject(rej); 443 | } 444 | }, 445 | apiMock.getDelay(), 446 | true // Trigger a $digest. 447 | ); 448 | 449 | return deferred.promise; 450 | }; 451 | }); 452 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-apimock", 3 | "version": "0.3.3", 4 | "description": "Automatically route your API calls to static JSON files, for hiccup free front–end development.", 5 | "authors": [ "John-Philip Johansson (http://johansson.jp/)" ], 6 | "homepage": "http://johansson.jp/angular-apimock/", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/seriema/angular-apimock.git" 10 | }, 11 | "main": "dist/angular-apimock.js", 12 | "keywords": [ 13 | "angular", 14 | "angularjs", 15 | "mock", 16 | "mocking", 17 | "apimock" 18 | ], 19 | "ignore": [ 20 | "**/*", 21 | "!dist/*", 22 | "!CHANGELOG.md", 23 | "!LICENSE", 24 | "!README.md", 25 | "!bower.json" 26 | ], 27 | "license": "MIT", 28 | "dependencies": { 29 | "angular": "^1.2.15" 30 | }, 31 | "devDependencies": { 32 | "angular-mocks": "^1.2.15", 33 | "angular-scenario": "^1.2.15" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /dist/angular-apimock.js: -------------------------------------------------------------------------------- 1 | /*! Angular API Mock v0.3.3 2 | * Licensed with MIT 3 | * Made with ♥ from Seriema + Redhorn */ 4 | /* Create the main module, `apiMock`. It's the one that needs to be included in 5 | * your projects. E.g. `angular.module('myApp', ['apiMock'])`. You don't need 6 | * to do anything else, but you can configure the paths for api-calls and mock 7 | * data by calling `app.config(function (apiMockProvider) { ... });`. 8 | */ 9 | angular.module('apiMock', []) 10 | 11 | .config(['$httpProvider', function ($httpProvider) { 12 | /* This is where the magic happens. Configure `$http` to use our 13 | `httpInterceptor` on all calls. It's what allows us to do automatic routing. 14 | */ 15 | $httpProvider.interceptors.push('httpInterceptor'); 16 | }]) 17 | 18 | .provider('apiMock', function () { 19 | /* This is the Provider for apiMock. It's used by `httpInterceptor` to support 20 | * mocking. 21 | * 22 | * Config options: 23 | * `mockDataPath` string: the path to be rerouted to. Default: `/mock_data`. 24 | * `apiPath` string: the path to be rerouted from. Default: `/api`. 25 | * 26 | * Public interface: 27 | * `onRequest` method: takes a `request` object and decides if mocking should 28 | * be done on this request. It checks global and local apiMock flags to see 29 | * if it should mock. It also checks the request URL if it starts with `apiPath`. 30 | * If the request is to have a `recover` attempt it's put in the fallbacks list. 31 | * A GET request to `/api/user/5?option=full` turns into `/mock_data/user/5.get.json`. 32 | * `onResponse` method: takes a `request` object and simply removes it from list 33 | * of fallbacks for `recover`. 34 | * `recover` method: if request has been marked for recover `onRequest` then it 35 | * will reroute to mock data. This is only to be called on response error. 36 | * 37 | * Private members: 38 | * `_countFallbacks` method: returns the current number of fallbacks in queue. 39 | * Only used for unit testing. 40 | */ 41 | 42 | // Helper objects 43 | // 44 | 45 | var $location; 46 | var $log; 47 | var $q; 48 | var $filter; 49 | var config = { 50 | defaultMock: false, 51 | mockDataPath: '/mock_data', 52 | apiPath: '/api', 53 | disable: false, 54 | stripQueries: true, 55 | delay: 0 56 | }; 57 | var fallbacks = []; 58 | 59 | // Helper methods 60 | // 61 | 62 | // TODO: IE8: remove when we drop IE8/Angular 1.2 support. 63 | // Object.keys isn't supported in IE8. Which we need to support as long as we support Angular 1.2. 64 | // This isn't a complete polyfill! It's just enough for what we need (and we don't need to bloat). 65 | function objectKeys(object) { 66 | var keys = []; 67 | 68 | angular.forEach(object, function (value, key) { 69 | keys.push(key); 70 | }); 71 | 72 | return keys; 73 | } 74 | 75 | // TODO: IE8: remove when we drop IE8/Angular 1.2 support. 76 | // Date.prototype.toISOString isn't supported in IE8. Which we need to support as long as we support Angular 1.2. 77 | // Modified from MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString 78 | function toISOString(date) { 79 | function pad(number) { 80 | if (number < 10) { 81 | return '0' + number; 82 | } 83 | return number; 84 | } 85 | 86 | return date.getUTCFullYear() + 87 | '-' + pad(date.getUTCMonth() + 1) + 88 | '-' + pad(date.getUTCDate()) + 89 | 'T' + pad(date.getUTCHours()) + 90 | '.' + pad(date.getUTCMinutes()) + 91 | '.' + pad(date.getUTCSeconds()) + 92 | '.' + (date.getUTCMilliseconds() / 1000).toFixed(3).slice(2, 5) + 93 | 'Z'; 94 | } 95 | 96 | // Taken as-is from Angular 1.4.x: https://github.com/angular/angular.js/blob/f13852c179ffd9ec18b7a94df27dec39eb5f19fc/src/Angular.js#L296 97 | function forEachSorted(obj, iterator, context) { 98 | var keys = objectKeys(obj).sort(); 99 | for (var i = 0; i < keys.length; i++) { 100 | iterator.call(context, obj[keys[i]], keys[i]); 101 | } 102 | return keys; 103 | } 104 | 105 | // Modified from Angular 1.4.x: https://github.com/angular/angular.js/blob/929ec6ba5a60e926654583033a90aebe716123c0/src/ng/http.js#L18 106 | function serializeValue(v) { 107 | if (angular.isDate(v)) { 108 | return toISOString(v); 109 | } 110 | 111 | return v; 112 | } 113 | 114 | // Modified from Angular 1.4.x: https://github.com/angular/angular.js/blob/720012eab6fef5e075a1d6876dd2e508c8e95b73/src/ngResource/resource.js#L405 115 | function encodeUriQuery(val) { 116 | return encodeURIComponent(val). 117 | replace(/%40/gi, '@'). 118 | replace(/%3A/gi, ':'). 119 | replace(/%24/g, '$'). 120 | replace(/%2C/gi, ','). 121 | replace(/%20/g, '+'); 122 | } 123 | 124 | // TODO: replace with a $httpParamSerializerJQLikeProvider() call when we require Angular 1.4 (i.e. when we drop 1.2 and 1.3). 125 | // Modified from Angular 1.4.x: https://github.com/angular/angular.js/blob/929ec6ba5a60e926654583033a90aebe716123c0/src/ng/http.js#L108 126 | function jQueryLikeParamSerializer(params) { 127 | var parts = []; 128 | 129 | function serialize(toSerialize, prefix, topLevel) { 130 | if (angular.isArray(toSerialize)) { 131 | // Serialize arrays. 132 | angular.forEach(toSerialize, function (value, index) { 133 | serialize(value, prefix + '[' + (angular.isObject(value) ? index : '') + ']'); 134 | }); 135 | } else if (angular.isObject(toSerialize) && !angular.isDate(toSerialize)) { 136 | // Serialize objects (not dates, because that's covered by the default case). 137 | forEachSorted(toSerialize, function (value, key) { 138 | serialize(value, prefix + 139 | (topLevel ? '' : '[') + 140 | key + 141 | (topLevel ? '' : ']')); 142 | }); 143 | } else if (toSerialize === undefined || toSerialize === '') { 144 | // Keep empty parameters as it still affects the mock file path. 145 | parts.push(encodeUriQuery(prefix)); 146 | } else { 147 | // Serialize everything else (including dates). 148 | parts.push(encodeUriQuery(prefix) + '=' + encodeUriQuery(serializeValue(toSerialize))); 149 | } 150 | } 151 | 152 | serialize(params, '', true); 153 | return parts.join('&'); 154 | } 155 | 156 | function queryStringToObject(paramString) { 157 | if (!paramString) { 158 | return {}; 159 | } 160 | 161 | var paramArray = paramString.split('&'); 162 | 163 | var result = {}; 164 | angular.forEach(paramArray, function (param) { 165 | param = param.split('='); 166 | result[param[0]] = param[1] || ''; 167 | }); 168 | 169 | return result; 170 | } 171 | 172 | function detectParameter(keys) { 173 | var regex = /apimock/i; 174 | var result; 175 | 176 | angular.forEach(keys, function (value, key) { 177 | if (regex.test(key)) { 178 | result = value; 179 | } 180 | }); 181 | 182 | return result; 183 | } 184 | 185 | function localMock(req) { 186 | return detectParameter(req); 187 | } 188 | 189 | function getParameter(req) { 190 | var mockValue = localMock(req); 191 | // Note: `false` is a valid option, so we can't use falsy-checks. 192 | if (mockValue === undefined) { 193 | mockValue = globalMock(); 194 | } 195 | if (mockValue === undefined) { 196 | mockValue = config.defaultMock; 197 | } 198 | 199 | return mockValue; 200 | } 201 | 202 | function getCommand(mockValue) { 203 | // Depending how we got mockValue it might've been parsed into a type or not. 204 | switch ((mockValue || '').toString().toLowerCase()) { 205 | case '200': 206 | case '404': 207 | case '500': 208 | return { type: 'respond', value: parseInt(mockValue, 10) }; 209 | 210 | case 'auto': 211 | return { type: 'recover' }; 212 | 213 | case 'true': 214 | return { type: 'reroute' }; 215 | } 216 | 217 | return { type: 'ignore' }; 218 | } 219 | 220 | 221 | function globalMock() { 222 | return detectParameter($location.search()); 223 | } 224 | 225 | function httpStatusResponse(status) { 226 | var response = { 227 | status: status, 228 | headers: { 229 | 'Content-Type': 'text/html; charset=utf-8', 230 | 'Server': 'Angular ApiMock' 231 | } 232 | }; 233 | $log.info('apiMock: mocking HTTP status to ' + status); 234 | return $q.reject(response); 235 | } 236 | 237 | function isApiPath(url) { 238 | return (apiPathMatched(url, config.apiPath) !== undefined); 239 | } 240 | 241 | function apiPathMatched(url, apiPath) { 242 | var match; // Lets initially assume undefined as no match 243 | 244 | if (angular.isArray(apiPath)) { 245 | angular.forEach(apiPath, function (path) { 246 | if (match) { return; } // Hack to skip more recursive calls if already matched 247 | var found = apiPathMatched(url, path); 248 | if (found) { 249 | match = found; 250 | } 251 | }); 252 | } 253 | if (match) { 254 | return match; 255 | } 256 | if (apiPath instanceof RegExp) { 257 | if (apiPath.test(url)) { 258 | return apiPath; 259 | } 260 | } 261 | if ((url.toString().indexOf(apiPath) === 0)) { 262 | return apiPath; 263 | } 264 | return match; 265 | } 266 | 267 | function prepareFallback(req) { 268 | if (isApiPath(req.url)) { 269 | fallbacks.push(req); 270 | } 271 | } 272 | 273 | function removeFallback(res) { 274 | var startLength = fallbacks.length; 275 | fallbacks = $filter('filter')(fallbacks, { 276 | method: '!' + res.method, 277 | url: '!' + res.url 278 | }, true); 279 | 280 | return startLength > fallbacks.length; 281 | } 282 | 283 | function reroute(req) { 284 | if (!isApiPath(req.url)) { 285 | return req; 286 | } 287 | 288 | // replace apiPath with mockDataPath. 289 | var oldPath = req.url; 290 | 291 | var redirectedPath = req.url.replace(apiPathMatched(req.url, config.apiPath), config.mockDataPath); 292 | 293 | var split = redirectedPath.split('?'); 294 | var newPath = split[0]; 295 | var queries = split[1] || ''; 296 | 297 | // query strings are stripped by default (like ?search=banana). 298 | if (!config.stripQueries) { 299 | 300 | //test if we have query params 301 | //if we do merge them on to the params object 302 | var queryParamsFromUrl = queryStringToObject(queries); 303 | var params = angular.extend(req.params || {}, queryParamsFromUrl); 304 | 305 | //test if there is already a trailing / 306 | if (newPath[newPath.length - 1] !== '/') { 307 | newPath += '/'; 308 | } 309 | 310 | //serialize the param object to convert to string 311 | //and concatenate to the newPath 312 | newPath += angular.lowercase(jQueryLikeParamSerializer(params)); 313 | } 314 | 315 | //Kill the params property so they aren't added back on to the end of the url 316 | req.params = undefined; 317 | 318 | // add file endings (method verb and .json). 319 | if (newPath[newPath.length - 1] === '/') { 320 | newPath = newPath.slice(0, -1); 321 | } 322 | newPath += '.' + req.method.toLowerCase() + '.json'; 323 | 324 | req.method = 'GET'; 325 | req.url = newPath; 326 | $log.info('apiMock: rerouting ' + oldPath + ' to ' + newPath); 327 | 328 | return req; 329 | } 330 | 331 | // Expose public interface for provider instance 332 | // 333 | 334 | function ApiMock(_$location, _$log, _$q, _$filter) { 335 | $location = _$location; 336 | $log = _$log; 337 | $q = _$q; 338 | $filter = _$filter; 339 | } 340 | 341 | var p = ApiMock.prototype; 342 | 343 | p._countFallbacks = function () { 344 | return fallbacks.length; 345 | }; 346 | 347 | p.getDelay = function () { 348 | return config.delay; 349 | }; 350 | 351 | p.onRequest = function (req) { 352 | if (config.disable) { 353 | return req; 354 | } 355 | 356 | var param = getParameter(req); 357 | var command = getCommand(param); 358 | 359 | switch (command.type) { 360 | case 'reroute': 361 | return reroute(req); 362 | case 'recover': 363 | prepareFallback(req); 364 | return req; 365 | case 'respond': 366 | return httpStatusResponse(command.value); 367 | case 'ignore': 368 | /* falls through */ 369 | default: 370 | return req; 371 | } 372 | }; 373 | 374 | p.onResponse = function (res) { 375 | removeFallback(res); 376 | return res; 377 | }; 378 | 379 | p.recover = function (rej) { 380 | if (config.disable) { 381 | return false; 382 | } 383 | 384 | if (rej.config === undefined) {// Why is this called with regular response object sometimes? 385 | return false; 386 | } 387 | 388 | if (removeFallback(rej.config)) { 389 | $log.info('apiMock: recovering from failure at ' + rej.config.url); 390 | return reroute(rej.config); 391 | } 392 | 393 | return false; 394 | }; 395 | 396 | // Expose Provider interface 397 | // 398 | 399 | this.config = function (options) { 400 | angular.extend(config, options); 401 | }; 402 | 403 | this.$get = ['$location', '$log', '$q', '$filter', function ($location, $log, $q, $filter) { 404 | return new ApiMock($location, $log, $q, $filter); 405 | }]; 406 | }) 407 | 408 | .service('httpInterceptor', ['$injector', '$q', '$timeout', 'apiMock', function ($injector, $q, $timeout, apiMock) { 409 | /* The main service. Is jacked in as a interceptor on `$http` so it gets called 410 | * on every http call. This allows us to do our magic. It uses the provider 411 | * `apiMock` to determine if a mock should be done, then do the actual mocking. 412 | */ 413 | this.request = function (req) { 414 | return apiMock.onRequest(req); 415 | }; 416 | 417 | this.response = function (res) { 418 | var deferred = $q.defer(); 419 | 420 | $timeout( 421 | function () { 422 | // TODO: Apparently, no tests break regardless what this resolves to. Fix the tests! 423 | deferred.resolve(apiMock.onResponse(res)); 424 | }, 425 | apiMock.getDelay(), 426 | true // Trigger a $digest. 427 | ); 428 | 429 | return deferred.promise; 430 | }; 431 | 432 | this.responseError = function (rej) { 433 | var deferred = $q.defer(); 434 | 435 | $timeout( 436 | function () { 437 | var recover = apiMock.recover(rej); 438 | 439 | if (recover) { 440 | var $http = $injector.get('$http'); 441 | $http(recover).then(function (data) { 442 | deferred.resolve(data); 443 | }); 444 | } else { 445 | deferred.reject(rej); 446 | } 447 | }, 448 | apiMock.getDelay(), 449 | true // Trigger a $digest. 450 | ); 451 | 452 | return deferred.promise; 453 | }; 454 | }]); 455 | -------------------------------------------------------------------------------- /dist/angular-apimock.min.js: -------------------------------------------------------------------------------- 1 | /*! Angular API Mock v0.3.3 2 | * Licensed with MIT 3 | * Made with ♥ from Seriema + Redhorn */ 4 | angular.module("apiMock",[]).config(["$httpProvider",function(a){a.interceptors.push("httpInterceptor")}]).provider("apiMock",function(){function a(a){var b=[];return angular.forEach(a,function(a,c){b.push(c)}),b}function b(a){function b(a){return 10>a?"0"+a:a}return a.getUTCFullYear()+"-"+b(a.getUTCMonth()+1)+"-"+b(a.getUTCDate())+"T"+b(a.getUTCHours())+"."+b(a.getUTCMinutes())+"."+b(a.getUTCSeconds())+"."+(a.getUTCMilliseconds()/1e3).toFixed(3).slice(2,5)+"Z"}function c(b,c,d){for(var e=a(b).sort(),f=0;fy.length}function r(a){if(!n(a.url))return a;var b=a.url,c=a.url.replace(o(a.url,x.apiPath),x.mockDataPath),d=c.split("?"),e=d[0],h=d[1]||"";if(!x.stripQueries){var i=g(h),j=angular.extend(a.params||{},i);"/"!==e[e.length-1]&&(e+="/"),e+=angular.lowercase(f(j))}return a.params=void 0,"/"===e[e.length-1]&&(e=e.slice(0,-1)),e+="."+a.method.toLowerCase()+".json",a.method="GET",a.url=e,u.info("apiMock: rerouting "+b+" to "+e),a}function s(a,b,c,d){t=a,u=b,v=c,w=d}var t,u,v,w,x={defaultMock:!1,mockDataPath:"/mock_data",apiPath:"/api",disable:!1,stripQueries:!0,delay:0},y=[],z=s.prototype;z._countFallbacks=function(){return y.length},z.getDelay=function(){return x.delay},z.onRequest=function(a){if(x.disable)return a;var b=j(a),c=k(b);switch(c.type){case"reroute":return r(a);case"recover":return p(a),a;case"respond":return m(c.value);case"ignore":default:return a}},z.onResponse=function(a){return q(a),a},z.recover=function(a){return x.disable?!1:void 0===a.config?!1:q(a.config)?(u.info("apiMock: recovering from failure at "+a.config.url),r(a.config)):!1},this.config=function(a){angular.extend(x,a)},this.$get=["$location","$log","$q","$filter",function(a,b,c,d){return new s(a,b,c,d)}]}).service("httpInterceptor",["$injector","$q","$timeout","apiMock",function(a,b,c,d){this.request=function(a){return d.onRequest(a)},this.response=function(a){var e=b.defer();return c(function(){e.resolve(d.onResponse(a))},d.getDelay(),!0),e.promise},this.responseError=function(e){var f=b.defer();return c(function(){var b=d.recover(e);if(b){var c=a.get("$http");c(b).then(function(a){f.resolve(a)})}else f.reject(e)},d.getDelay(),!0),f.promise}}]); -------------------------------------------------------------------------------- /karma-e2e.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.10/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | // base path, that will be used to resolve files and exclude 7 | basePath: '', 8 | 9 | // testing framework to use (jasmine/mocha/qunit/...) 10 | frameworks: [ 'ng-scenario' ], 11 | 12 | // list of files / patterns to load in the browser 13 | files: [ 14 | 'test/e2e/**/*.js' 15 | ], 16 | 17 | // list of files / patterns to exclude 18 | exclude: [], 19 | 20 | // web server port 21 | port: 8080, 22 | 23 | // level of logging 24 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 25 | logLevel: config.LOG_INFO, 26 | 27 | 28 | // enable / disable watching file and executing tests whenever any file changes 29 | autoWatch: false, 30 | 31 | 32 | // Start these browsers, currently available: 33 | // - Chrome 34 | // - ChromeCanary 35 | // - Firefox 36 | // - Opera 37 | // - Safari (only Mac) 38 | // - PhantomJS 39 | // - IE (only Windows) 40 | browsers: [ 'Firefox' ], 41 | 42 | 43 | // Continuous Integration mode 44 | // if true, it capture browsers, run tests and exit 45 | singleRun: false 46 | 47 | // Uncomment the following lines if you are using grunt's server to run the tests 48 | // proxies: { 49 | // '/': 'http://localhost:9000/' 50 | // }, 51 | // URL root prevent conflicts with the site root 52 | // urlRoot: '_karma_' 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.10/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | var sourcePreprocessors = [ 'coverage' ]; 6 | function isDebug(argument) { 7 | return argument === '--debug'; 8 | } 9 | if (process.argv.some(isDebug)) { 10 | sourcePreprocessors = []; 11 | } 12 | 13 | config.set({ 14 | // base path, that will be used to resolve files and exclude 15 | basePath: '', 16 | 17 | // testing framework to use (jasmine/mocha/qunit/...) 18 | frameworks: [ 'jasmine' ], 19 | 20 | // reporter style 21 | reporters: [ 'progress' ], 22 | 23 | preprocessors: { 24 | // source files, that you wanna generate coverage for 25 | // do not include tests or libraries 26 | // (these files will be instrumented by Istanbul) 27 | 'app/scripts/**/*.js': sourcePreprocessors 28 | }, 29 | 30 | // list of files / patterns to load in the browser 31 | files: [ 32 | 'app/bower_components/angular/angular.js', 33 | 'app/bower_components/angular-mocks/angular-mocks.js', 34 | 'app/scripts/**/*.js', 35 | 'test/spec/**/*.js' 36 | ], 37 | 38 | // list of files / patterns to exclude 39 | exclude: [], 40 | 41 | // web server port 42 | port: 9876, // default 43 | 44 | // level of logging 45 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 46 | logLevel: config.LOG_INFO, 47 | 48 | 49 | // enable / disable watching file and executing tests whenever any file changes 50 | autoWatch: false, 51 | 52 | 53 | // Start these browsers, currently available: 54 | // - Chrome 55 | // - ChromeCanary 56 | // - Firefox 57 | // - Opera 58 | // - Safari (only Mac) 59 | // - PhantomJS 60 | // - IE (only Windows) 61 | browsers: [ 'PhantomJS' ], 62 | 63 | // Check out https://saucelabs.com/platforms for all browser/platform combos 64 | captureTimeout: 120000, 65 | customLaunchers: { 66 | 'SL_Chrome': { 67 | base: 'SauceLabs', 68 | browserName: 'chrome' 69 | }, 70 | 'SL_Firefox': { 71 | base: 'SauceLabs', 72 | browserName: 'firefox' 73 | }, 74 | 'SL_Safari': { 75 | base: 'SauceLabs', 76 | browserName: 'safari' 77 | }, 78 | 'SL_IE_8': { 79 | base: 'SauceLabs', 80 | browserName: 'internet explorer', 81 | version: '8' 82 | }, 83 | 'SL_IE_9': { 84 | base: 'SauceLabs', 85 | browserName: 'internet explorer', 86 | version: '9' 87 | }, 88 | 'SL_IE_10': { 89 | base: 'SauceLabs', 90 | browserName: 'internet explorer', 91 | version: '10' 92 | }, 93 | 'SL_IE_11': { 94 | base: 'SauceLabs', 95 | browserName: 'internet explorer', 96 | platform: 'Windows 8.1', 97 | version: '11' 98 | }, 99 | 'SL_iOS': { 100 | base: 'SauceLabs', 101 | browserName: 'iphone' 102 | } 103 | }, 104 | 105 | // SauceLabs config for local development. 106 | // You need to set `SAUCE_USERNAME` and `SAUCE_ACCESS_KEY` as environment variables. 107 | sauceLabs: { 108 | testName: 'Angular ApiMock', 109 | recordVideo: false, 110 | recordScreenshots: false, 111 | startConnect: true 112 | }, 113 | 114 | // Continuous Integration mode 115 | // if true, it capture browsers, run tests and exit 116 | singleRun: false 117 | }); 118 | 119 | // Travis specific configs. 120 | if (process.env.TRAVIS) { 121 | var buildLabel = 'TRAVIS #' + process.env.TRAVIS_BUILD_NUMBER + ' (' + process.env.TRAVIS_BUILD_ID + ')'; 122 | 123 | config.logLevel = config.LOG_DEBUG; 124 | 125 | config.sauceLabs.build = buildLabel; 126 | config.sauceLabs.startConnect = false; 127 | config.sauceLabs.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER; 128 | config.sauceLabs.recordScreenshots = true; 129 | 130 | // Allocating a browser can take pretty long (eg. if we are out of capacity and need to wait 131 | // for another build to finish) and so the `captureTimeout` typically kills 132 | // an in-queue-pending request, which makes no sense. 133 | config.captureTimeout = 0; 134 | 135 | // Debug logging into a file, that we print out at the end of the build. 136 | config.loggers.push({ 137 | type: 'file', 138 | filename: (process.env.LOGS_DIR || '') + '/karma.log' 139 | }); 140 | } 141 | }; 142 | -------------------------------------------------------------------------------- /nuget/Angular-ApiMock.0.1.6.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seriema/angular-apimock/a6c4042f15a471cebb5b26155a41c33725ba8e60/nuget/Angular-ApiMock.0.1.6.nupkg -------------------------------------------------------------------------------- /nuget/Angular-ApiMock.0.1.7.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seriema/angular-apimock/a6c4042f15a471cebb5b26155a41c33725ba8e60/nuget/Angular-ApiMock.0.1.7.nupkg -------------------------------------------------------------------------------- /nuget/Angular-ApiMock.0.1.8.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seriema/angular-apimock/a6c4042f15a471cebb5b26155a41c33725ba8e60/nuget/Angular-ApiMock.0.1.8.nupkg -------------------------------------------------------------------------------- /nuget/Angular-ApiMock.0.2.0.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seriema/angular-apimock/a6c4042f15a471cebb5b26155a41c33725ba8e60/nuget/Angular-ApiMock.0.2.0.nupkg -------------------------------------------------------------------------------- /nuget/Angular-ApiMock.0.2.1.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seriema/angular-apimock/a6c4042f15a471cebb5b26155a41c33725ba8e60/nuget/Angular-ApiMock.0.2.1.nupkg -------------------------------------------------------------------------------- /nuget/Angular-ApiMock.0.3.0.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seriema/angular-apimock/a6c4042f15a471cebb5b26155a41c33725ba8e60/nuget/Angular-ApiMock.0.3.0.nupkg -------------------------------------------------------------------------------- /nuget/Angular-ApiMock.0.3.1.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seriema/angular-apimock/a6c4042f15a471cebb5b26155a41c33725ba8e60/nuget/Angular-ApiMock.0.3.1.nupkg -------------------------------------------------------------------------------- /nuget/Angular-ApiMock.0.3.2.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seriema/angular-apimock/a6c4042f15a471cebb5b26155a41c33725ba8e60/nuget/Angular-ApiMock.0.3.2.nupkg -------------------------------------------------------------------------------- /nuget/Angular-ApiMock.0.3.3.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seriema/angular-apimock/a6c4042f15a471cebb5b26155a41c33725ba8e60/nuget/Angular-ApiMock.0.3.3.nupkg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-apimock", 3 | "version": "0.3.3", 4 | "description": "Automatically route your API calls to static JSON files, for hiccup free front–end development.", 5 | "author": "John-Philip Johansson (http://johansson.jp/)", 6 | "homepage": "http://johansson.jp/angular-apimock/", 7 | "bugs": "https://github.com/seriema/angular-apimock/issues", 8 | "repository": { 9 | "type": "git", 10 | "url": "git://github.com/seriema/angular-apimock.git" 11 | }, 12 | "keywords": [ 13 | "angular", 14 | "angularjs", 15 | "mock", 16 | "mocking", 17 | "apimock" 18 | ], 19 | "files": [ 20 | "dist/", 21 | "CHANGELOG.md", 22 | "LICENSE", 23 | "README.md", 24 | "package.json" 25 | ], 26 | "main": "dist/angular-apimock.js", 27 | "license": "MIT", 28 | "dependencies": {}, 29 | "devDependencies": { 30 | "grunt": "^1.0.1", 31 | "grunt-bump": "^0.8.0", 32 | "grunt-contrib-clean": "^1.0.0", 33 | "grunt-contrib-concat": "^1.0.1", 34 | "grunt-contrib-connect": "^1.0.2", 35 | "grunt-contrib-jshint": "^1.0.0", 36 | "grunt-contrib-uglify": "^2.0.0", 37 | "grunt-contrib-watch": "^1.0.0", 38 | "grunt-conventional-changelog": "^6.1.0", 39 | "grunt-conventional-github-releaser": "^1.0.0", 40 | "grunt-coveralls": "^1.0.0", 41 | "grunt-git": "^1.0.0", 42 | "grunt-jscs": "^3.0.1", 43 | "grunt-karma": "^2.0.0", 44 | "grunt-newer": "^1.1.0", 45 | "grunt-ng-annotate": "^2.0.2", 46 | "grunt-npm": "0.0.2", 47 | "grunt-nsp-package": "0.0.5", 48 | "grunt-nuget": "^0.1.3", 49 | "jasmine": "^2.3.2", 50 | "jasmine-core": "^2.3.4", 51 | "jshint-stylish": "^2.0.1", 52 | "karma": "^1.2.0", 53 | "karma-chrome-launcher": "^2.0.0", 54 | "karma-coverage": "^1.1.1", 55 | "karma-firefox-launcher": "^1.0.0", 56 | "karma-jasmine": "^1.0.2", 57 | "karma-ng-scenario": "^1.0.0", 58 | "karma-phantomjs-launcher": "^1.0.0", 59 | "karma-sauce-launcher": "^1.0.0", 60 | "karma-script-launcher": "^1.0.0", 61 | "load-grunt-tasks": "^3.1.0", 62 | "phantomjs-prebuilt": "^2.1.4", 63 | "time-grunt": "^1.0.0" 64 | }, 65 | "engines": { 66 | "node": ">=0.12.0" 67 | }, 68 | "scripts": { 69 | "test": "grunt test", 70 | "debug": "karma start karma.conf.js --browsers=Chrome --debug --autoWatch=true", 71 | "saucelabs": "grunt karma:sauce", 72 | "coveralls": "grunt coveralls" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /package.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Angular-ApiMock 5 | 0.0.0 6 | Angular ApiMock 7 | John-Philip Johansson, Joel Lundberg 8 | John-Philip Johansson, Joel Lundberg 9 | https://github.com/seriema/angular-apimock/blob/master/LICENSE 10 | https://github.com/seriema/angular-apimock 11 | false 12 | Automatically route your API calls to static JSON files, for hiccup free front–end development. 13 | Automatically route your API calls to static JSON files, for hiccup free front–end development. 14 | en-US 15 | javascript angular angularjs mock 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "expr": true, 10 | "immed": true, 11 | "indent": 2, 12 | "latedef": true, 13 | "newcap": true, 14 | "noarg": true, 15 | "quotmark": "single", 16 | "regexp": true, 17 | "undef": true, 18 | "unused": true, 19 | "strict": true, 20 | "trailing": true, 21 | "smarttabs": true, 22 | "globals": { 23 | "after": false, 24 | "afterEach": false, 25 | "angular": false, 26 | "before": false, 27 | "beforeEach": false, 28 | "browser": false, 29 | "describe": false, 30 | "expect": false, 31 | "inject": false, 32 | "it": false, 33 | "fail": false, 34 | "jasmine": false, 35 | "spyOn": false 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/ref/angular-mocks-v1.2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.2.28 3 | * (c) 2010-2014 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular, undefined) { 7 | 8 | 'use strict'; 9 | 10 | /** 11 | * @ngdoc object 12 | * @name angular.mock 13 | * @description 14 | * 15 | * Namespace from 'angular-mocks.js' which contains testing related code. 16 | */ 17 | angular.mock = {}; 18 | 19 | /** 20 | * ! This is a private undocumented service ! 21 | * 22 | * @name $browser 23 | * 24 | * @description 25 | * This service is a mock implementation of {@link ng.$browser}. It provides fake 26 | * implementation for commonly used browser apis that are hard to test, e.g. setTimeout, xhr, 27 | * cookies, etc... 28 | * 29 | * The api of this service is the same as that of the real {@link ng.$browser $browser}, except 30 | * that there are several helper methods available which can be used in tests. 31 | */ 32 | angular.mock.$BrowserProvider = function() { 33 | this.$get = function() { 34 | return new angular.mock.$Browser(); 35 | }; 36 | }; 37 | 38 | angular.mock.$Browser = function() { 39 | var self = this; 40 | 41 | this.isMock = true; 42 | self.$$url = "http://server/"; 43 | self.$$lastUrl = self.$$url; // used by url polling fn 44 | self.pollFns = []; 45 | 46 | // TODO(vojta): remove this temporary api 47 | self.$$completeOutstandingRequest = angular.noop; 48 | self.$$incOutstandingRequestCount = angular.noop; 49 | 50 | 51 | // register url polling fn 52 | 53 | self.onUrlChange = function(listener) { 54 | self.pollFns.push( 55 | function() { 56 | if (self.$$lastUrl != self.$$url) { 57 | self.$$lastUrl = self.$$url; 58 | listener(self.$$url); 59 | } 60 | } 61 | ); 62 | 63 | return listener; 64 | }; 65 | 66 | self.$$checkUrlChange = angular.noop; 67 | 68 | self.cookieHash = {}; 69 | self.lastCookieHash = {}; 70 | self.deferredFns = []; 71 | self.deferredNextId = 0; 72 | 73 | self.defer = function(fn, delay) { 74 | delay = delay || 0; 75 | self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId}); 76 | self.deferredFns.sort(function(a,b){ return a.time - b.time;}); 77 | return self.deferredNextId++; 78 | }; 79 | 80 | 81 | /** 82 | * @name $browser#defer.now 83 | * 84 | * @description 85 | * Current milliseconds mock time. 86 | */ 87 | self.defer.now = 0; 88 | 89 | 90 | self.defer.cancel = function(deferId) { 91 | var fnIndex; 92 | 93 | angular.forEach(self.deferredFns, function(fn, index) { 94 | if (fn.id === deferId) fnIndex = index; 95 | }); 96 | 97 | if (fnIndex !== undefined) { 98 | self.deferredFns.splice(fnIndex, 1); 99 | return true; 100 | } 101 | 102 | return false; 103 | }; 104 | 105 | 106 | /** 107 | * @name $browser#defer.flush 108 | * 109 | * @description 110 | * Flushes all pending requests and executes the defer callbacks. 111 | * 112 | * @param {number=} number of milliseconds to flush. See {@link #defer.now} 113 | */ 114 | self.defer.flush = function(delay) { 115 | if (angular.isDefined(delay)) { 116 | self.defer.now += delay; 117 | } else { 118 | if (self.deferredFns.length) { 119 | self.defer.now = self.deferredFns[self.deferredFns.length-1].time; 120 | } else { 121 | throw new Error('No deferred tasks to be flushed'); 122 | } 123 | } 124 | 125 | while (self.deferredFns.length && self.deferredFns[0].time <= self.defer.now) { 126 | self.deferredFns.shift().fn(); 127 | } 128 | }; 129 | 130 | self.$$baseHref = ''; 131 | self.baseHref = function() { 132 | return this.$$baseHref; 133 | }; 134 | }; 135 | angular.mock.$Browser.prototype = { 136 | 137 | /** 138 | * @name $browser#poll 139 | * 140 | * @description 141 | * run all fns in pollFns 142 | */ 143 | poll: function poll() { 144 | angular.forEach(this.pollFns, function(pollFn){ 145 | pollFn(); 146 | }); 147 | }, 148 | 149 | addPollFn: function(pollFn) { 150 | this.pollFns.push(pollFn); 151 | return pollFn; 152 | }, 153 | 154 | url: function(url, replace) { 155 | if (url) { 156 | this.$$url = url; 157 | return this; 158 | } 159 | 160 | return this.$$url; 161 | }, 162 | 163 | cookies: function(name, value) { 164 | if (name) { 165 | if (angular.isUndefined(value)) { 166 | delete this.cookieHash[name]; 167 | } else { 168 | if (angular.isString(value) && //strings only 169 | value.length <= 4096) { //strict cookie storage limits 170 | this.cookieHash[name] = value; 171 | } 172 | } 173 | } else { 174 | if (!angular.equals(this.cookieHash, this.lastCookieHash)) { 175 | this.lastCookieHash = angular.copy(this.cookieHash); 176 | this.cookieHash = angular.copy(this.cookieHash); 177 | } 178 | return this.cookieHash; 179 | } 180 | }, 181 | 182 | notifyWhenNoOutstandingRequests: function(fn) { 183 | fn(); 184 | } 185 | }; 186 | 187 | 188 | /** 189 | * @ngdoc provider 190 | * @name $exceptionHandlerProvider 191 | * 192 | * @description 193 | * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors 194 | * passed into the `$exceptionHandler`. 195 | */ 196 | 197 | /** 198 | * @ngdoc service 199 | * @name $exceptionHandler 200 | * 201 | * @description 202 | * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed 203 | * into it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration 204 | * information. 205 | * 206 | * 207 | * ```js 208 | * describe('$exceptionHandlerProvider', function() { 209 | * 210 | * it('should capture log messages and exceptions', function() { 211 | * 212 | * module(function($exceptionHandlerProvider) { 213 | * $exceptionHandlerProvider.mode('log'); 214 | * }); 215 | * 216 | * inject(function($log, $exceptionHandler, $timeout) { 217 | * $timeout(function() { $log.log(1); }); 218 | * $timeout(function() { $log.log(2); throw 'banana peel'; }); 219 | * $timeout(function() { $log.log(3); }); 220 | * expect($exceptionHandler.errors).toEqual([]); 221 | * expect($log.assertEmpty()); 222 | * $timeout.flush(); 223 | * expect($exceptionHandler.errors).toEqual(['banana peel']); 224 | * expect($log.log.logs).toEqual([[1], [2], [3]]); 225 | * }); 226 | * }); 227 | * }); 228 | * ``` 229 | */ 230 | 231 | angular.mock.$ExceptionHandlerProvider = function() { 232 | var handler; 233 | 234 | /** 235 | * @ngdoc method 236 | * @name $exceptionHandlerProvider#mode 237 | * 238 | * @description 239 | * Sets the logging mode. 240 | * 241 | * @param {string} mode Mode of operation, defaults to `rethrow`. 242 | * 243 | * - `rethrow`: If any errors are passed into the handler in tests, it typically 244 | * means that there is a bug in the application or test, so this mock will 245 | * make these tests fail. 246 | * - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log` 247 | * mode stores an array of errors in `$exceptionHandler.errors`, to allow later 248 | * assertion of them. See {@link ngMock.$log#assertEmpty assertEmpty()} and 249 | * {@link ngMock.$log#reset reset()} 250 | */ 251 | this.mode = function(mode) { 252 | switch(mode) { 253 | case 'rethrow': 254 | handler = function(e) { 255 | throw e; 256 | }; 257 | break; 258 | case 'log': 259 | var errors = []; 260 | 261 | handler = function(e) { 262 | if (arguments.length == 1) { 263 | errors.push(e); 264 | } else { 265 | errors.push([].slice.call(arguments, 0)); 266 | } 267 | }; 268 | 269 | handler.errors = errors; 270 | break; 271 | default: 272 | throw new Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!"); 273 | } 274 | }; 275 | 276 | this.$get = function() { 277 | return handler; 278 | }; 279 | 280 | this.mode('rethrow'); 281 | }; 282 | 283 | 284 | /** 285 | * @ngdoc service 286 | * @name $log 287 | * 288 | * @description 289 | * Mock implementation of {@link ng.$log} that gathers all logged messages in arrays 290 | * (one array per logging level). These arrays are exposed as `logs` property of each of the 291 | * level-specific log function, e.g. for level `error` the array is exposed as `$log.error.logs`. 292 | * 293 | */ 294 | angular.mock.$LogProvider = function() { 295 | var debug = true; 296 | 297 | function concat(array1, array2, index) { 298 | return array1.concat(Array.prototype.slice.call(array2, index)); 299 | } 300 | 301 | this.debugEnabled = function(flag) { 302 | if (angular.isDefined(flag)) { 303 | debug = flag; 304 | return this; 305 | } else { 306 | return debug; 307 | } 308 | }; 309 | 310 | this.$get = function () { 311 | var $log = { 312 | log: function() { $log.log.logs.push(concat([], arguments, 0)); }, 313 | warn: function() { $log.warn.logs.push(concat([], arguments, 0)); }, 314 | info: function() { $log.info.logs.push(concat([], arguments, 0)); }, 315 | error: function() { $log.error.logs.push(concat([], arguments, 0)); }, 316 | debug: function() { 317 | if (debug) { 318 | $log.debug.logs.push(concat([], arguments, 0)); 319 | } 320 | } 321 | }; 322 | 323 | /** 324 | * @ngdoc method 325 | * @name $log#reset 326 | * 327 | * @description 328 | * Reset all of the logging arrays to empty. 329 | */ 330 | $log.reset = function () { 331 | /** 332 | * @ngdoc property 333 | * @name $log#log.logs 334 | * 335 | * @description 336 | * Array of messages logged using {@link ngMock.$log#log}. 337 | * 338 | * @example 339 | * ```js 340 | * $log.log('Some Log'); 341 | * var first = $log.log.logs.unshift(); 342 | * ``` 343 | */ 344 | $log.log.logs = []; 345 | /** 346 | * @ngdoc property 347 | * @name $log#info.logs 348 | * 349 | * @description 350 | * Array of messages logged using {@link ngMock.$log#info}. 351 | * 352 | * @example 353 | * ```js 354 | * $log.info('Some Info'); 355 | * var first = $log.info.logs.unshift(); 356 | * ``` 357 | */ 358 | $log.info.logs = []; 359 | /** 360 | * @ngdoc property 361 | * @name $log#warn.logs 362 | * 363 | * @description 364 | * Array of messages logged using {@link ngMock.$log#warn}. 365 | * 366 | * @example 367 | * ```js 368 | * $log.warn('Some Warning'); 369 | * var first = $log.warn.logs.unshift(); 370 | * ``` 371 | */ 372 | $log.warn.logs = []; 373 | /** 374 | * @ngdoc property 375 | * @name $log#error.logs 376 | * 377 | * @description 378 | * Array of messages logged using {@link ngMock.$log#error}. 379 | * 380 | * @example 381 | * ```js 382 | * $log.error('Some Error'); 383 | * var first = $log.error.logs.unshift(); 384 | * ``` 385 | */ 386 | $log.error.logs = []; 387 | /** 388 | * @ngdoc property 389 | * @name $log#debug.logs 390 | * 391 | * @description 392 | * Array of messages logged using {@link ngMock.$log#debug}. 393 | * 394 | * @example 395 | * ```js 396 | * $log.debug('Some Error'); 397 | * var first = $log.debug.logs.unshift(); 398 | * ``` 399 | */ 400 | $log.debug.logs = []; 401 | }; 402 | 403 | /** 404 | * @ngdoc method 405 | * @name $log#assertEmpty 406 | * 407 | * @description 408 | * Assert that the all of the logging methods have no logged messages. If messages present, an 409 | * exception is thrown. 410 | */ 411 | $log.assertEmpty = function() { 412 | var errors = []; 413 | angular.forEach(['error', 'warn', 'info', 'log', 'debug'], function(logLevel) { 414 | angular.forEach($log[logLevel].logs, function(log) { 415 | angular.forEach(log, function (logItem) { 416 | errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' + 417 | (logItem.stack || '')); 418 | }); 419 | }); 420 | }); 421 | if (errors.length) { 422 | errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or "+ 423 | "an expected log message was not checked and removed:"); 424 | errors.push(''); 425 | throw new Error(errors.join('\n---------\n')); 426 | } 427 | }; 428 | 429 | $log.reset(); 430 | return $log; 431 | }; 432 | }; 433 | 434 | 435 | /** 436 | * @ngdoc service 437 | * @name $interval 438 | * 439 | * @description 440 | * Mock implementation of the $interval service. 441 | * 442 | * Use {@link ngMock.$interval#flush `$interval.flush(millis)`} to 443 | * move forward by `millis` milliseconds and trigger any functions scheduled to run in that 444 | * time. 445 | * 446 | * @param {function()} fn A function that should be called repeatedly. 447 | * @param {number} delay Number of milliseconds between each function call. 448 | * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat 449 | * indefinitely. 450 | * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise 451 | * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. 452 | * @returns {promise} A promise which will be notified on each iteration. 453 | */ 454 | angular.mock.$IntervalProvider = function() { 455 | this.$get = ['$rootScope', '$q', 456 | function($rootScope, $q) { 457 | var repeatFns = [], 458 | nextRepeatId = 0, 459 | now = 0; 460 | 461 | var $interval = function(fn, delay, count, invokeApply) { 462 | var deferred = $q.defer(), 463 | promise = deferred.promise, 464 | iteration = 0, 465 | skipApply = (angular.isDefined(invokeApply) && !invokeApply); 466 | 467 | count = (angular.isDefined(count)) ? count : 0; 468 | promise.then(null, null, fn); 469 | 470 | promise.$$intervalId = nextRepeatId; 471 | 472 | function tick() { 473 | deferred.notify(iteration++); 474 | 475 | if (count > 0 && iteration >= count) { 476 | var fnIndex; 477 | deferred.resolve(iteration); 478 | 479 | angular.forEach(repeatFns, function(fn, index) { 480 | if (fn.id === promise.$$intervalId) fnIndex = index; 481 | }); 482 | 483 | if (fnIndex !== undefined) { 484 | repeatFns.splice(fnIndex, 1); 485 | } 486 | } 487 | 488 | if (!skipApply) $rootScope.$apply(); 489 | } 490 | 491 | repeatFns.push({ 492 | nextTime:(now + delay), 493 | delay: delay, 494 | fn: tick, 495 | id: nextRepeatId, 496 | deferred: deferred 497 | }); 498 | repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;}); 499 | 500 | nextRepeatId++; 501 | return promise; 502 | }; 503 | /** 504 | * @ngdoc method 505 | * @name $interval#cancel 506 | * 507 | * @description 508 | * Cancels a task associated with the `promise`. 509 | * 510 | * @param {promise} promise A promise from calling the `$interval` function. 511 | * @returns {boolean} Returns `true` if the task was successfully cancelled. 512 | */ 513 | $interval.cancel = function(promise) { 514 | if(!promise) return false; 515 | var fnIndex; 516 | 517 | angular.forEach(repeatFns, function(fn, index) { 518 | if (fn.id === promise.$$intervalId) fnIndex = index; 519 | }); 520 | 521 | if (fnIndex !== undefined) { 522 | repeatFns[fnIndex].deferred.reject('canceled'); 523 | repeatFns.splice(fnIndex, 1); 524 | return true; 525 | } 526 | 527 | return false; 528 | }; 529 | 530 | /** 531 | * @ngdoc method 532 | * @name $interval#flush 533 | * @description 534 | * 535 | * Runs interval tasks scheduled to be run in the next `millis` milliseconds. 536 | * 537 | * @param {number=} millis maximum timeout amount to flush up until. 538 | * 539 | * @return {number} The amount of time moved forward. 540 | */ 541 | $interval.flush = function(millis) { 542 | now += millis; 543 | while (repeatFns.length && repeatFns[0].nextTime <= now) { 544 | var task = repeatFns[0]; 545 | task.fn(); 546 | task.nextTime += task.delay; 547 | repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;}); 548 | } 549 | return millis; 550 | }; 551 | 552 | return $interval; 553 | }]; 554 | }; 555 | 556 | 557 | /* jshint -W101 */ 558 | /* The R_ISO8061_STR regex is never going to fit into the 100 char limit! 559 | * This directive should go inside the anonymous function but a bug in JSHint means that it would 560 | * not be enacted early enough to prevent the warning. 561 | */ 562 | var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; 563 | 564 | function jsonStringToDate(string) { 565 | var match; 566 | if (match = string.match(R_ISO8061_STR)) { 567 | var date = new Date(0), 568 | tzHour = 0, 569 | tzMin = 0; 570 | if (match[9]) { 571 | tzHour = int(match[9] + match[10]); 572 | tzMin = int(match[9] + match[11]); 573 | } 574 | date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3])); 575 | date.setUTCHours(int(match[4]||0) - tzHour, 576 | int(match[5]||0) - tzMin, 577 | int(match[6]||0), 578 | int(match[7]||0)); 579 | return date; 580 | } 581 | return string; 582 | } 583 | 584 | function int(str) { 585 | return parseInt(str, 10); 586 | } 587 | 588 | function padNumber(num, digits, trim) { 589 | var neg = ''; 590 | if (num < 0) { 591 | neg = '-'; 592 | num = -num; 593 | } 594 | num = '' + num; 595 | while(num.length < digits) num = '0' + num; 596 | if (trim) 597 | num = num.substr(num.length - digits); 598 | return neg + num; 599 | } 600 | 601 | 602 | /** 603 | * @ngdoc type 604 | * @name angular.mock.TzDate 605 | * @description 606 | * 607 | * *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`. 608 | * 609 | * Mock of the Date type which has its timezone specified via constructor arg. 610 | * 611 | * The main purpose is to create Date-like instances with timezone fixed to the specified timezone 612 | * offset, so that we can test code that depends on local timezone settings without dependency on 613 | * the time zone settings of the machine where the code is running. 614 | * 615 | * @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored) 616 | * @param {(number|string)} timestamp Timestamp representing the desired time in *UTC* 617 | * 618 | * @example 619 | * !!!! WARNING !!!!! 620 | * This is not a complete Date object so only methods that were implemented can be called safely. 621 | * To make matters worse, TzDate instances inherit stuff from Date via a prototype. 622 | * 623 | * We do our best to intercept calls to "unimplemented" methods, but since the list of methods is 624 | * incomplete we might be missing some non-standard methods. This can result in errors like: 625 | * "Date.prototype.foo called on incompatible Object". 626 | * 627 | * ```js 628 | * var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z'); 629 | * newYearInBratislava.getTimezoneOffset() => -60; 630 | * newYearInBratislava.getFullYear() => 2010; 631 | * newYearInBratislava.getMonth() => 0; 632 | * newYearInBratislava.getDate() => 1; 633 | * newYearInBratislava.getHours() => 0; 634 | * newYearInBratislava.getMinutes() => 0; 635 | * newYearInBratislava.getSeconds() => 0; 636 | * ``` 637 | * 638 | */ 639 | angular.mock.TzDate = function (offset, timestamp) { 640 | var self = new Date(0); 641 | if (angular.isString(timestamp)) { 642 | var tsStr = timestamp; 643 | 644 | self.origDate = jsonStringToDate(timestamp); 645 | 646 | timestamp = self.origDate.getTime(); 647 | if (isNaN(timestamp)) 648 | throw { 649 | name: "Illegal Argument", 650 | message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string" 651 | }; 652 | } else { 653 | self.origDate = new Date(timestamp); 654 | } 655 | 656 | var localOffset = new Date(timestamp).getTimezoneOffset(); 657 | self.offsetDiff = localOffset*60*1000 - offset*1000*60*60; 658 | self.date = new Date(timestamp + self.offsetDiff); 659 | 660 | self.getTime = function() { 661 | return self.date.getTime() - self.offsetDiff; 662 | }; 663 | 664 | self.toLocaleDateString = function() { 665 | return self.date.toLocaleDateString(); 666 | }; 667 | 668 | self.getFullYear = function() { 669 | return self.date.getFullYear(); 670 | }; 671 | 672 | self.getMonth = function() { 673 | return self.date.getMonth(); 674 | }; 675 | 676 | self.getDate = function() { 677 | return self.date.getDate(); 678 | }; 679 | 680 | self.getHours = function() { 681 | return self.date.getHours(); 682 | }; 683 | 684 | self.getMinutes = function() { 685 | return self.date.getMinutes(); 686 | }; 687 | 688 | self.getSeconds = function() { 689 | return self.date.getSeconds(); 690 | }; 691 | 692 | self.getMilliseconds = function() { 693 | return self.date.getMilliseconds(); 694 | }; 695 | 696 | self.getTimezoneOffset = function() { 697 | return offset * 60; 698 | }; 699 | 700 | self.getUTCFullYear = function() { 701 | return self.origDate.getUTCFullYear(); 702 | }; 703 | 704 | self.getUTCMonth = function() { 705 | return self.origDate.getUTCMonth(); 706 | }; 707 | 708 | self.getUTCDate = function() { 709 | return self.origDate.getUTCDate(); 710 | }; 711 | 712 | self.getUTCHours = function() { 713 | return self.origDate.getUTCHours(); 714 | }; 715 | 716 | self.getUTCMinutes = function() { 717 | return self.origDate.getUTCMinutes(); 718 | }; 719 | 720 | self.getUTCSeconds = function() { 721 | return self.origDate.getUTCSeconds(); 722 | }; 723 | 724 | self.getUTCMilliseconds = function() { 725 | return self.origDate.getUTCMilliseconds(); 726 | }; 727 | 728 | self.getDay = function() { 729 | return self.date.getDay(); 730 | }; 731 | 732 | // provide this method only on browsers that already have it 733 | if (self.toISOString) { 734 | self.toISOString = function() { 735 | return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + 736 | padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + 737 | padNumber(self.origDate.getUTCDate(), 2) + 'T' + 738 | padNumber(self.origDate.getUTCHours(), 2) + ':' + 739 | padNumber(self.origDate.getUTCMinutes(), 2) + ':' + 740 | padNumber(self.origDate.getUTCSeconds(), 2) + '.' + 741 | padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z'; 742 | }; 743 | } 744 | 745 | //hide all methods not implemented in this mock that the Date prototype exposes 746 | var unimplementedMethods = ['getUTCDay', 747 | 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', 748 | 'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear', 749 | 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds', 750 | 'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString', 751 | 'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf']; 752 | 753 | angular.forEach(unimplementedMethods, function(methodName) { 754 | self[methodName] = function() { 755 | throw new Error("Method '" + methodName + "' is not implemented in the TzDate mock"); 756 | }; 757 | }); 758 | 759 | return self; 760 | }; 761 | 762 | //make "tzDateInstance instanceof Date" return true 763 | angular.mock.TzDate.prototype = Date.prototype; 764 | /* jshint +W101 */ 765 | 766 | angular.mock.animate = angular.module('ngAnimateMock', ['ng']) 767 | 768 | .config(['$provide', function($provide) { 769 | 770 | var reflowQueue = []; 771 | $provide.value('$$animateReflow', function(fn) { 772 | var index = reflowQueue.length; 773 | reflowQueue.push(fn); 774 | return function cancel() { 775 | reflowQueue.splice(index, 1); 776 | }; 777 | }); 778 | 779 | $provide.decorator('$animate', function($delegate, $$asyncCallback) { 780 | var animate = { 781 | queue : [], 782 | enabled : $delegate.enabled, 783 | triggerCallbacks : function() { 784 | $$asyncCallback.flush(); 785 | }, 786 | triggerReflow : function() { 787 | angular.forEach(reflowQueue, function(fn) { 788 | fn(); 789 | }); 790 | reflowQueue = []; 791 | } 792 | }; 793 | 794 | angular.forEach( 795 | ['enter','leave','move','addClass','removeClass','setClass'], function(method) { 796 | animate[method] = function() { 797 | animate.queue.push({ 798 | event : method, 799 | element : arguments[0], 800 | args : arguments 801 | }); 802 | $delegate[method].apply($delegate, arguments); 803 | }; 804 | }); 805 | 806 | return animate; 807 | }); 808 | 809 | }]); 810 | 811 | 812 | /** 813 | * @ngdoc function 814 | * @name angular.mock.dump 815 | * @description 816 | * 817 | * *NOTE*: this is not an injectable instance, just a globally available function. 818 | * 819 | * Method for serializing common angular objects (scope, elements, etc..) into strings, useful for 820 | * debugging. 821 | * 822 | * This method is also available on window, where it can be used to display objects on debug 823 | * console. 824 | * 825 | * @param {*} object - any object to turn into string. 826 | * @return {string} a serialized string of the argument 827 | */ 828 | angular.mock.dump = function(object) { 829 | return serialize(object); 830 | 831 | function serialize(object) { 832 | var out; 833 | 834 | if (angular.isElement(object)) { 835 | object = angular.element(object); 836 | out = angular.element('
'); 837 | angular.forEach(object, function(element) { 838 | out.append(angular.element(element).clone()); 839 | }); 840 | out = out.html(); 841 | } else if (angular.isArray(object)) { 842 | out = []; 843 | angular.forEach(object, function(o) { 844 | out.push(serialize(o)); 845 | }); 846 | out = '[ ' + out.join(', ') + ' ]'; 847 | } else if (angular.isObject(object)) { 848 | if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) { 849 | out = serializeScope(object); 850 | } else if (object instanceof Error) { 851 | out = object.stack || ('' + object.name + ': ' + object.message); 852 | } else { 853 | // TODO(i): this prevents methods being logged, 854 | // we should have a better way to serialize objects 855 | out = angular.toJson(object, true); 856 | } 857 | } else { 858 | out = String(object); 859 | } 860 | 861 | return out; 862 | } 863 | 864 | function serializeScope(scope, offset) { 865 | offset = offset || ' '; 866 | var log = [offset + 'Scope(' + scope.$id + '): {']; 867 | for ( var key in scope ) { 868 | if (Object.prototype.hasOwnProperty.call(scope, key) && !key.match(/^(\$|this)/)) { 869 | log.push(' ' + key + ': ' + angular.toJson(scope[key])); 870 | } 871 | } 872 | var child = scope.$$childHead; 873 | while(child) { 874 | log.push(serializeScope(child, offset + ' ')); 875 | child = child.$$nextSibling; 876 | } 877 | log.push('}'); 878 | return log.join('\n' + offset); 879 | } 880 | }; 881 | 882 | /** 883 | * @ngdoc service 884 | * @name $httpBackend 885 | * @description 886 | * Fake HTTP backend implementation suitable for unit testing applications that use the 887 | * {@link ng.$http $http service}. 888 | * 889 | * *Note*: For fake HTTP backend implementation suitable for end-to-end testing or backend-less 890 | * development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}. 891 | * 892 | * During unit testing, we want our unit tests to run quickly and have no external dependencies so 893 | * we don’t want to send [XHR](https://developer.mozilla.org/en/xmlhttprequest) or 894 | * [JSONP](http://en.wikipedia.org/wiki/JSONP) requests to a real server. All we really need is 895 | * to verify whether a certain request has been sent or not, or alternatively just let the 896 | * application make requests, respond with pre-trained responses and assert that the end result is 897 | * what we expect it to be. 898 | * 899 | * This mock implementation can be used to respond with static or dynamic responses via the 900 | * `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc). 901 | * 902 | * When an Angular application needs some data from a server, it calls the $http service, which 903 | * sends the request to a real server using $httpBackend service. With dependency injection, it is 904 | * easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify 905 | * the requests and respond with some testing data without sending a request to a real server. 906 | * 907 | * There are two ways to specify what test data should be returned as http responses by the mock 908 | * backend when the code under test makes http requests: 909 | * 910 | * - `$httpBackend.expect` - specifies a request expectation 911 | * - `$httpBackend.when` - specifies a backend definition 912 | * 913 | * 914 | * # Request Expectations vs Backend Definitions 915 | * 916 | * Request expectations provide a way to make assertions about requests made by the application and 917 | * to define responses for those requests. The test will fail if the expected requests are not made 918 | * or they are made in the wrong order. 919 | * 920 | * Backend definitions allow you to define a fake backend for your application which doesn't assert 921 | * if a particular request was made or not, it just returns a trained response if a request is made. 922 | * The test will pass whether or not the request gets made during testing. 923 | * 924 | * 925 | * 926 | * 927 | * 928 | * 929 | * 930 | * 931 | * 932 | * 933 | * 934 | * 935 | * 936 | * 937 | * 938 | * 939 | * 940 | * 941 | * 942 | * 943 | * 944 | * 945 | * 946 | * 947 | * 948 | * 949 | * 950 | * 951 | * 952 | * 953 | * 954 | * 955 | * 956 | * 957 | *
Request expectationsBackend definitions
Syntax.expect(...).respond(...).when(...).respond(...)
Typical usagestrict unit testsloose (black-box) unit testing
Fulfills multiple requestsNOYES
Order of requests mattersYESNO
Request requiredYESNO
Response requiredoptional (see below)YES
958 | * 959 | * In cases where both backend definitions and request expectations are specified during unit 960 | * testing, the request expectations are evaluated first. 961 | * 962 | * If a request expectation has no response specified, the algorithm will search your backend 963 | * definitions for an appropriate response. 964 | * 965 | * If a request didn't match any expectation or if the expectation doesn't have the response 966 | * defined, the backend definitions are evaluated in sequential order to see if any of them match 967 | * the request. The response from the first matched definition is returned. 968 | * 969 | * 970 | * # Flushing HTTP requests 971 | * 972 | * The $httpBackend used in production always responds to requests asynchronously. If we preserved 973 | * this behavior in unit testing, we'd have to create async unit tests, which are hard to write, 974 | * to follow and to maintain. But neither can the testing mock respond synchronously; that would 975 | * change the execution of the code under test. For this reason, the mock $httpBackend has a 976 | * `flush()` method, which allows the test to explicitly flush pending requests. This preserves 977 | * the async api of the backend, while allowing the test to execute synchronously. 978 | * 979 | * 980 | * # Unit testing with mock $httpBackend 981 | * The following code shows how to setup and use the mock backend when unit testing a controller. 982 | * First we create the controller under test: 983 | * 984 | ```js 985 | // The controller code 986 | function MyController($scope, $http) { 987 | var authToken; 988 | 989 | $http.get('/auth.py').success(function(data, status, headers) { 990 | authToken = headers('A-Token'); 991 | $scope.user = data; 992 | }); 993 | 994 | $scope.saveMessage = function(message) { 995 | var headers = { 'Authorization': authToken }; 996 | $scope.status = 'Saving...'; 997 | 998 | $http.post('/add-msg.py', message, { headers: headers } ).success(function(response) { 999 | $scope.status = ''; 1000 | }).error(function() { 1001 | $scope.status = 'ERROR!'; 1002 | }); 1003 | }; 1004 | } 1005 | ``` 1006 | * 1007 | * Now we setup the mock backend and create the test specs: 1008 | * 1009 | ```js 1010 | // testing controller 1011 | describe('MyController', function() { 1012 | var $httpBackend, $rootScope, createController; 1013 | 1014 | beforeEach(inject(function($injector) { 1015 | // Set up the mock http service responses 1016 | $httpBackend = $injector.get('$httpBackend'); 1017 | // backend definition common for all tests 1018 | $httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'}); 1019 | 1020 | // Get hold of a scope (i.e. the root scope) 1021 | $rootScope = $injector.get('$rootScope'); 1022 | // The $controller service is used to create instances of controllers 1023 | var $controller = $injector.get('$controller'); 1024 | 1025 | createController = function() { 1026 | return $controller('MyController', {'$scope' : $rootScope }); 1027 | }; 1028 | })); 1029 | 1030 | 1031 | afterEach(function() { 1032 | $httpBackend.verifyNoOutstandingExpectation(); 1033 | $httpBackend.verifyNoOutstandingRequest(); 1034 | }); 1035 | 1036 | 1037 | it('should fetch authentication token', function() { 1038 | $httpBackend.expectGET('/auth.py'); 1039 | var controller = createController(); 1040 | $httpBackend.flush(); 1041 | }); 1042 | 1043 | 1044 | it('should send msg to server', function() { 1045 | var controller = createController(); 1046 | $httpBackend.flush(); 1047 | 1048 | // now you don’t care about the authentication, but 1049 | // the controller will still send the request and 1050 | // $httpBackend will respond without you having to 1051 | // specify the expectation and response for this request 1052 | 1053 | $httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, ''); 1054 | $rootScope.saveMessage('message content'); 1055 | expect($rootScope.status).toBe('Saving...'); 1056 | $httpBackend.flush(); 1057 | expect($rootScope.status).toBe(''); 1058 | }); 1059 | 1060 | 1061 | it('should send auth header', function() { 1062 | var controller = createController(); 1063 | $httpBackend.flush(); 1064 | 1065 | $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) { 1066 | // check if the header was send, if it wasn't the expectation won't 1067 | // match the request and the test will fail 1068 | return headers['Authorization'] == 'xxx'; 1069 | }).respond(201, ''); 1070 | 1071 | $rootScope.saveMessage('whatever'); 1072 | $httpBackend.flush(); 1073 | }); 1074 | }); 1075 | ``` 1076 | */ 1077 | angular.mock.$HttpBackendProvider = function() { 1078 | this.$get = ['$rootScope', createHttpBackendMock]; 1079 | }; 1080 | 1081 | /** 1082 | * General factory function for $httpBackend mock. 1083 | * Returns instance for unit testing (when no arguments specified): 1084 | * - passing through is disabled 1085 | * - auto flushing is disabled 1086 | * 1087 | * Returns instance for e2e testing (when `$delegate` and `$browser` specified): 1088 | * - passing through (delegating request to real backend) is enabled 1089 | * - auto flushing is enabled 1090 | * 1091 | * @param {Object=} $delegate Real $httpBackend instance (allow passing through if specified) 1092 | * @param {Object=} $browser Auto-flushing enabled if specified 1093 | * @return {Object} Instance of $httpBackend mock 1094 | */ 1095 | function createHttpBackendMock($rootScope, $delegate, $browser) { 1096 | var definitions = [], 1097 | expectations = [], 1098 | responses = [], 1099 | responsesPush = angular.bind(responses, responses.push), 1100 | copy = angular.copy; 1101 | 1102 | function createResponse(status, data, headers, statusText) { 1103 | if (angular.isFunction(status)) return status; 1104 | 1105 | return function() { 1106 | return angular.isNumber(status) 1107 | ? [status, data, headers, statusText] 1108 | : [200, status, data]; 1109 | }; 1110 | } 1111 | 1112 | // TODO(vojta): change params to: method, url, data, headers, callback 1113 | function $httpBackend(method, url, data, callback, headers, timeout, withCredentials) { 1114 | var xhr = new MockXhr(), 1115 | expectation = expectations[0], 1116 | wasExpected = false; 1117 | 1118 | function prettyPrint(data) { 1119 | return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) 1120 | ? data 1121 | : angular.toJson(data); 1122 | } 1123 | 1124 | function wrapResponse(wrapped) { 1125 | if (!$browser && timeout && timeout.then) timeout.then(handleTimeout); 1126 | 1127 | return handleResponse; 1128 | 1129 | function handleResponse() { 1130 | var response = wrapped.response(method, url, data, headers); 1131 | xhr.$$respHeaders = response[2]; 1132 | callback(copy(response[0]), copy(response[1]), xhr.getAllResponseHeaders(), 1133 | copy(response[3] || '')); 1134 | } 1135 | 1136 | function handleTimeout() { 1137 | for (var i = 0, ii = responses.length; i < ii; i++) { 1138 | if (responses[i] === handleResponse) { 1139 | responses.splice(i, 1); 1140 | callback(-1, undefined, ''); 1141 | break; 1142 | } 1143 | } 1144 | } 1145 | } 1146 | 1147 | if (expectation && expectation.match(method, url)) { 1148 | if (!expectation.matchData(data)) 1149 | throw new Error('Expected ' + expectation + ' with different data\n' + 1150 | 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); 1151 | 1152 | if (!expectation.matchHeaders(headers)) 1153 | throw new Error('Expected ' + expectation + ' with different headers\n' + 1154 | 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + 1155 | prettyPrint(headers)); 1156 | 1157 | expectations.shift(); 1158 | 1159 | if (expectation.response) { 1160 | responses.push(wrapResponse(expectation)); 1161 | return; 1162 | } 1163 | wasExpected = true; 1164 | } 1165 | 1166 | var i = -1, definition; 1167 | while ((definition = definitions[++i])) { 1168 | if (definition.match(method, url, data, headers || {})) { 1169 | if (definition.response) { 1170 | // if $browser specified, we do auto flush all requests 1171 | ($browser ? $browser.defer : responsesPush)(wrapResponse(definition)); 1172 | } else if (definition.passThrough) { 1173 | $delegate(method, url, data, callback, headers, timeout, withCredentials); 1174 | } else throw new Error('No response defined !'); 1175 | return; 1176 | } 1177 | } 1178 | throw wasExpected ? 1179 | new Error('No response defined !') : 1180 | new Error('Unexpected request: ' + method + ' ' + url + '\n' + 1181 | (expectation ? 'Expected ' + expectation : 'No more request expected')); 1182 | } 1183 | 1184 | /** 1185 | * @ngdoc method 1186 | * @name $httpBackend#when 1187 | * @description 1188 | * Creates a new backend definition. 1189 | * 1190 | * @param {string} method HTTP method. 1191 | * @param {string|RegExp} url HTTP url. 1192 | * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives 1193 | * data string and returns true if the data is as expected. 1194 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 1195 | * object and returns true if the headers match the current definition. 1196 | * @returns {requestHandler} Returns an object with a `respond` method that controls how a matched 1197 | * request is handled. 1198 | * 1199 | * - respond – 1200 | * `{function([status,] data[, headers, statusText]) 1201 | * | function(function(method, url, data, headers)}` 1202 | * – The respond method takes a set of static data to be returned or a function that can 1203 | * return an array containing response status (number), response data (string), response 1204 | * headers (Object), and the text for the status (string). 1205 | */ 1206 | $httpBackend.when = function(method, url, data, headers) { 1207 | var definition = new MockHttpExpectation(method, url, data, headers), 1208 | chain = { 1209 | respond: function(status, data, headers, statusText) { 1210 | definition.response = createResponse(status, data, headers, statusText); 1211 | } 1212 | }; 1213 | 1214 | if ($browser) { 1215 | chain.passThrough = function() { 1216 | definition.passThrough = true; 1217 | }; 1218 | } 1219 | 1220 | definitions.push(definition); 1221 | return chain; 1222 | }; 1223 | 1224 | /** 1225 | * @ngdoc method 1226 | * @name $httpBackend#whenGET 1227 | * @description 1228 | * Creates a new backend definition for GET requests. For more info see `when()`. 1229 | * 1230 | * @param {string|RegExp} url HTTP url. 1231 | * @param {(Object|function(Object))=} headers HTTP headers. 1232 | * @returns {requestHandler} Returns an object with a `respond` method that controls how a matched 1233 | * request is handled. 1234 | */ 1235 | 1236 | /** 1237 | * @ngdoc method 1238 | * @name $httpBackend#whenHEAD 1239 | * @description 1240 | * Creates a new backend definition for HEAD requests. For more info see `when()`. 1241 | * 1242 | * @param {string|RegExp} url HTTP url. 1243 | * @param {(Object|function(Object))=} headers HTTP headers. 1244 | * @returns {requestHandler} Returns an object with a `respond` method that controls how a matched 1245 | * request is handled. 1246 | */ 1247 | 1248 | /** 1249 | * @ngdoc method 1250 | * @name $httpBackend#whenDELETE 1251 | * @description 1252 | * Creates a new backend definition for DELETE requests. For more info see `when()`. 1253 | * 1254 | * @param {string|RegExp} url HTTP url. 1255 | * @param {(Object|function(Object))=} headers HTTP headers. 1256 | * @returns {requestHandler} Returns an object with a `respond` method that controls how a matched 1257 | * request is handled. 1258 | */ 1259 | 1260 | /** 1261 | * @ngdoc method 1262 | * @name $httpBackend#whenPOST 1263 | * @description 1264 | * Creates a new backend definition for POST requests. For more info see `when()`. 1265 | * 1266 | * @param {string|RegExp} url HTTP url. 1267 | * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives 1268 | * data string and returns true if the data is as expected. 1269 | * @param {(Object|function(Object))=} headers HTTP headers. 1270 | * @returns {requestHandler} Returns an object with a `respond` method that controls how a matched 1271 | * request is handled. 1272 | */ 1273 | 1274 | /** 1275 | * @ngdoc method 1276 | * @name $httpBackend#whenPUT 1277 | * @description 1278 | * Creates a new backend definition for PUT requests. For more info see `when()`. 1279 | * 1280 | * @param {string|RegExp} url HTTP url. 1281 | * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives 1282 | * data string and returns true if the data is as expected. 1283 | * @param {(Object|function(Object))=} headers HTTP headers. 1284 | * @returns {requestHandler} Returns an object with a `respond` method that controls how a matched 1285 | * request is handled. 1286 | */ 1287 | 1288 | /** 1289 | * @ngdoc method 1290 | * @name $httpBackend#whenPATCH 1291 | * @description 1292 | * Creates a new backend definition for PATCH requests. For more info see `when()`. 1293 | * 1294 | * @param {string|RegExp} url HTTP url. 1295 | * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives 1296 | * data string and returns true if the data is as expected. 1297 | * @param {(Object|function(Object))=} headers HTTP headers. 1298 | * @returns {requestHandler} Returns an object with a `respond` method that controls how a matched 1299 | * request is handled. 1300 | */ 1301 | 1302 | /** 1303 | * @ngdoc method 1304 | * @name $httpBackend#whenJSONP 1305 | * @description 1306 | * Creates a new backend definition for JSONP requests. For more info see `when()`. 1307 | * 1308 | * @param {string|RegExp} url HTTP url. 1309 | * @returns {requestHandler} Returns an object with a `respond` method that controls how a matched 1310 | * request is handled. 1311 | */ 1312 | createShortMethods('when'); 1313 | 1314 | 1315 | /** 1316 | * @ngdoc method 1317 | * @name $httpBackend#expect 1318 | * @description 1319 | * Creates a new request expectation. 1320 | * 1321 | * @param {string} method HTTP method. 1322 | * @param {string|RegExp} url HTTP url. 1323 | * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that 1324 | * receives data string and returns true if the data is as expected, or Object if request body 1325 | * is in JSON format. 1326 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 1327 | * object and returns true if the headers match the current expectation. 1328 | * @returns {requestHandler} Returns an object with a `respond` method that controls how a matched 1329 | * request is handled. 1330 | * 1331 | * - respond – 1332 | * `{function([status,] data[, headers, statusText]) 1333 | * | function(function(method, url, data, headers)}` 1334 | * – The respond method takes a set of static data to be returned or a function that can 1335 | * return an array containing response status (number), response data (string), response 1336 | * headers (Object), and the text for the status (string). 1337 | */ 1338 | $httpBackend.expect = function(method, url, data, headers) { 1339 | var expectation = new MockHttpExpectation(method, url, data, headers); 1340 | expectations.push(expectation); 1341 | return { 1342 | respond: function (status, data, headers, statusText) { 1343 | expectation.response = createResponse(status, data, headers, statusText); 1344 | } 1345 | }; 1346 | }; 1347 | 1348 | 1349 | /** 1350 | * @ngdoc method 1351 | * @name $httpBackend#expectGET 1352 | * @description 1353 | * Creates a new request expectation for GET requests. For more info see `expect()`. 1354 | * 1355 | * @param {string|RegExp} url HTTP url. 1356 | * @param {Object=} headers HTTP headers. 1357 | * @returns {requestHandler} Returns an object with a `respond` method that controls how a matched 1358 | * request is handled. See #expect for more info. 1359 | */ 1360 | 1361 | /** 1362 | * @ngdoc method 1363 | * @name $httpBackend#expectHEAD 1364 | * @description 1365 | * Creates a new request expectation for HEAD requests. For more info see `expect()`. 1366 | * 1367 | * @param {string|RegExp} url HTTP url. 1368 | * @param {Object=} headers HTTP headers. 1369 | * @returns {requestHandler} Returns an object with a `respond` method that controls how a matched 1370 | * request is handled. 1371 | */ 1372 | 1373 | /** 1374 | * @ngdoc method 1375 | * @name $httpBackend#expectDELETE 1376 | * @description 1377 | * Creates a new request expectation for DELETE requests. For more info see `expect()`. 1378 | * 1379 | * @param {string|RegExp} url HTTP url. 1380 | * @param {Object=} headers HTTP headers. 1381 | * @returns {requestHandler} Returns an object with a `respond` method that controls how a matched 1382 | * request is handled. 1383 | */ 1384 | 1385 | /** 1386 | * @ngdoc method 1387 | * @name $httpBackend#expectPOST 1388 | * @description 1389 | * Creates a new request expectation for POST requests. For more info see `expect()`. 1390 | * 1391 | * @param {string|RegExp} url HTTP url. 1392 | * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that 1393 | * receives data string and returns true if the data is as expected, or Object if request body 1394 | * is in JSON format. 1395 | * @param {Object=} headers HTTP headers. 1396 | * @returns {requestHandler} Returns an object with a `respond` method that controls how a matched 1397 | * request is handled. 1398 | */ 1399 | 1400 | /** 1401 | * @ngdoc method 1402 | * @name $httpBackend#expectPUT 1403 | * @description 1404 | * Creates a new request expectation for PUT requests. For more info see `expect()`. 1405 | * 1406 | * @param {string|RegExp} url HTTP url. 1407 | * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that 1408 | * receives data string and returns true if the data is as expected, or Object if request body 1409 | * is in JSON format. 1410 | * @param {Object=} headers HTTP headers. 1411 | * @returns {requestHandler} Returns an object with a `respond` method that controls how a matched 1412 | * request is handled. 1413 | */ 1414 | 1415 | /** 1416 | * @ngdoc method 1417 | * @name $httpBackend#expectPATCH 1418 | * @description 1419 | * Creates a new request expectation for PATCH requests. For more info see `expect()`. 1420 | * 1421 | * @param {string|RegExp} url HTTP url. 1422 | * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that 1423 | * receives data string and returns true if the data is as expected, or Object if request body 1424 | * is in JSON format. 1425 | * @param {Object=} headers HTTP headers. 1426 | * @returns {requestHandler} Returns an object with a `respond` method that controls how a matched 1427 | * request is handled. 1428 | */ 1429 | 1430 | /** 1431 | * @ngdoc method 1432 | * @name $httpBackend#expectJSONP 1433 | * @description 1434 | * Creates a new request expectation for JSONP requests. For more info see `expect()`. 1435 | * 1436 | * @param {string|RegExp} url HTTP url. 1437 | * @returns {requestHandler} Returns an object with a `respond` method that controls how a matched 1438 | * request is handled. 1439 | */ 1440 | createShortMethods('expect'); 1441 | 1442 | 1443 | /** 1444 | * @ngdoc method 1445 | * @name $httpBackend#flush 1446 | * @description 1447 | * Flushes all pending requests using the trained responses. 1448 | * 1449 | * @param {number=} count Number of responses to flush (in the order they arrived). If undefined, 1450 | * all pending requests will be flushed. If there are no pending requests when the flush method 1451 | * is called an exception is thrown (as this typically a sign of programming error). 1452 | */ 1453 | $httpBackend.flush = function(count) { 1454 | $rootScope.$digest(); 1455 | if (!responses.length) throw new Error('No pending request to flush !'); 1456 | 1457 | if (angular.isDefined(count)) { 1458 | while (count--) { 1459 | if (!responses.length) throw new Error('No more pending request to flush !'); 1460 | responses.shift()(); 1461 | } 1462 | } else { 1463 | while (responses.length) { 1464 | responses.shift()(); 1465 | } 1466 | } 1467 | $httpBackend.verifyNoOutstandingExpectation(); 1468 | }; 1469 | 1470 | 1471 | /** 1472 | * @ngdoc method 1473 | * @name $httpBackend#verifyNoOutstandingExpectation 1474 | * @description 1475 | * Verifies that all of the requests defined via the `expect` api were made. If any of the 1476 | * requests were not made, verifyNoOutstandingExpectation throws an exception. 1477 | * 1478 | * Typically, you would call this method following each test case that asserts requests using an 1479 | * "afterEach" clause. 1480 | * 1481 | * ```js 1482 | * afterEach($httpBackend.verifyNoOutstandingExpectation); 1483 | * ``` 1484 | */ 1485 | $httpBackend.verifyNoOutstandingExpectation = function() { 1486 | $rootScope.$digest(); 1487 | if (expectations.length) { 1488 | throw new Error('Unsatisfied requests: ' + expectations.join(', ')); 1489 | } 1490 | }; 1491 | 1492 | 1493 | /** 1494 | * @ngdoc method 1495 | * @name $httpBackend#verifyNoOutstandingRequest 1496 | * @description 1497 | * Verifies that there are no outstanding requests that need to be flushed. 1498 | * 1499 | * Typically, you would call this method following each test case that asserts requests using an 1500 | * "afterEach" clause. 1501 | * 1502 | * ```js 1503 | * afterEach($httpBackend.verifyNoOutstandingRequest); 1504 | * ``` 1505 | */ 1506 | $httpBackend.verifyNoOutstandingRequest = function() { 1507 | if (responses.length) { 1508 | throw new Error('Unflushed requests: ' + responses.length); 1509 | } 1510 | }; 1511 | 1512 | 1513 | /** 1514 | * @ngdoc method 1515 | * @name $httpBackend#resetExpectations 1516 | * @description 1517 | * Resets all request expectations, but preserves all backend definitions. Typically, you would 1518 | * call resetExpectations during a multiple-phase test when you want to reuse the same instance of 1519 | * $httpBackend mock. 1520 | */ 1521 | $httpBackend.resetExpectations = function() { 1522 | expectations.length = 0; 1523 | responses.length = 0; 1524 | }; 1525 | 1526 | return $httpBackend; 1527 | 1528 | 1529 | function createShortMethods(prefix) { 1530 | angular.forEach(['GET', 'DELETE', 'JSONP', 'HEAD'], function(method) { 1531 | $httpBackend[prefix + method] = function(url, headers) { 1532 | return $httpBackend[prefix](method, url, undefined, headers); 1533 | }; 1534 | }); 1535 | 1536 | angular.forEach(['PUT', 'POST', 'PATCH'], function(method) { 1537 | $httpBackend[prefix + method] = function(url, data, headers) { 1538 | return $httpBackend[prefix](method, url, data, headers); 1539 | }; 1540 | }); 1541 | } 1542 | } 1543 | 1544 | function MockHttpExpectation(method, url, data, headers) { 1545 | 1546 | this.data = data; 1547 | this.headers = headers; 1548 | 1549 | this.match = function(m, u, d, h) { 1550 | if (method != m) return false; 1551 | if (!this.matchUrl(u)) return false; 1552 | if (angular.isDefined(d) && !this.matchData(d)) return false; 1553 | if (angular.isDefined(h) && !this.matchHeaders(h)) return false; 1554 | return true; 1555 | }; 1556 | 1557 | this.matchUrl = function(u) { 1558 | if (!url) return true; 1559 | if (angular.isFunction(url.test)) return url.test(u); 1560 | return url == u; 1561 | }; 1562 | 1563 | this.matchHeaders = function(h) { 1564 | if (angular.isUndefined(headers)) return true; 1565 | if (angular.isFunction(headers)) return headers(h); 1566 | return angular.equals(headers, h); 1567 | }; 1568 | 1569 | this.matchData = function(d) { 1570 | if (angular.isUndefined(data)) return true; 1571 | if (data && angular.isFunction(data.test)) return data.test(d); 1572 | if (data && angular.isFunction(data)) return data(d); 1573 | if (data && !angular.isString(data)) { 1574 | return angular.equals(angular.fromJson(angular.toJson(data)), angular.fromJson(d)); 1575 | } 1576 | return data == d; 1577 | }; 1578 | 1579 | this.toString = function() { 1580 | return method + ' ' + url; 1581 | }; 1582 | } 1583 | 1584 | function createMockXhr() { 1585 | return new MockXhr(); 1586 | } 1587 | 1588 | function MockXhr() { 1589 | 1590 | // hack for testing $http, $httpBackend 1591 | MockXhr.$$lastInstance = this; 1592 | 1593 | this.open = function(method, url, async) { 1594 | this.$$method = method; 1595 | this.$$url = url; 1596 | this.$$async = async; 1597 | this.$$reqHeaders = {}; 1598 | this.$$respHeaders = {}; 1599 | }; 1600 | 1601 | this.send = function(data) { 1602 | this.$$data = data; 1603 | }; 1604 | 1605 | this.setRequestHeader = function(key, value) { 1606 | this.$$reqHeaders[key] = value; 1607 | }; 1608 | 1609 | this.getResponseHeader = function(name) { 1610 | // the lookup must be case insensitive, 1611 | // that's why we try two quick lookups first and full scan last 1612 | var header = this.$$respHeaders[name]; 1613 | if (header) return header; 1614 | 1615 | name = angular.lowercase(name); 1616 | header = this.$$respHeaders[name]; 1617 | if (header) return header; 1618 | 1619 | header = undefined; 1620 | angular.forEach(this.$$respHeaders, function(headerVal, headerName) { 1621 | if (!header && angular.lowercase(headerName) == name) header = headerVal; 1622 | }); 1623 | return header; 1624 | }; 1625 | 1626 | this.getAllResponseHeaders = function() { 1627 | var lines = []; 1628 | 1629 | angular.forEach(this.$$respHeaders, function(value, key) { 1630 | lines.push(key + ': ' + value); 1631 | }); 1632 | return lines.join('\n'); 1633 | }; 1634 | 1635 | this.abort = angular.noop; 1636 | } 1637 | 1638 | 1639 | /** 1640 | * @ngdoc service 1641 | * @name $timeout 1642 | * @description 1643 | * 1644 | * This service is just a simple decorator for {@link ng.$timeout $timeout} service 1645 | * that adds a "flush" and "verifyNoPendingTasks" methods. 1646 | */ 1647 | 1648 | angular.mock.$TimeoutDecorator = function($delegate, $browser) { 1649 | 1650 | /** 1651 | * @ngdoc method 1652 | * @name $timeout#flush 1653 | * @description 1654 | * 1655 | * Flushes the queue of pending tasks. 1656 | * 1657 | * @param {number=} delay maximum timeout amount to flush up until 1658 | */ 1659 | $delegate.flush = function(delay) { 1660 | $browser.defer.flush(delay); 1661 | }; 1662 | 1663 | /** 1664 | * @ngdoc method 1665 | * @name $timeout#verifyNoPendingTasks 1666 | * @description 1667 | * 1668 | * Verifies that there are no pending tasks that need to be flushed. 1669 | */ 1670 | $delegate.verifyNoPendingTasks = function() { 1671 | if ($browser.deferredFns.length) { 1672 | throw new Error('Deferred tasks to flush (' + $browser.deferredFns.length + '): ' + 1673 | formatPendingTasksAsString($browser.deferredFns)); 1674 | } 1675 | }; 1676 | 1677 | function formatPendingTasksAsString(tasks) { 1678 | var result = []; 1679 | angular.forEach(tasks, function(task) { 1680 | result.push('{id: ' + task.id + ', ' + 'time: ' + task.time + '}'); 1681 | }); 1682 | 1683 | return result.join(', '); 1684 | } 1685 | 1686 | return $delegate; 1687 | }; 1688 | 1689 | angular.mock.$RAFDecorator = function($delegate) { 1690 | var queue = []; 1691 | var rafFn = function(fn) { 1692 | var index = queue.length; 1693 | queue.push(fn); 1694 | return function() { 1695 | queue.splice(index, 1); 1696 | }; 1697 | }; 1698 | 1699 | rafFn.supported = $delegate.supported; 1700 | 1701 | rafFn.flush = function() { 1702 | if(queue.length === 0) { 1703 | throw new Error('No rAF callbacks present'); 1704 | } 1705 | 1706 | var length = queue.length; 1707 | for(var i=0;i'); 1737 | }; 1738 | }; 1739 | 1740 | /** 1741 | * @ngdoc module 1742 | * @name ngMock 1743 | * @packageName angular-mocks 1744 | * @description 1745 | * 1746 | * # ngMock 1747 | * 1748 | * The `ngMock` module provides support to inject and mock Angular services into unit tests. 1749 | * In addition, ngMock also extends various core ng services such that they can be 1750 | * inspected and controlled in a synchronous manner within test code. 1751 | * 1752 | * 1753 | *
1754 | * 1755 | */ 1756 | angular.module('ngMock', ['ng']).provider({ 1757 | $browser: angular.mock.$BrowserProvider, 1758 | $exceptionHandler: angular.mock.$ExceptionHandlerProvider, 1759 | $log: angular.mock.$LogProvider, 1760 | $interval: angular.mock.$IntervalProvider, 1761 | $httpBackend: angular.mock.$HttpBackendProvider, 1762 | $rootElement: angular.mock.$RootElementProvider 1763 | }).config(['$provide', function($provide) { 1764 | $provide.decorator('$timeout', angular.mock.$TimeoutDecorator); 1765 | $provide.decorator('$$rAF', angular.mock.$RAFDecorator); 1766 | $provide.decorator('$$asyncCallback', angular.mock.$AsyncCallbackDecorator); 1767 | }]); 1768 | 1769 | /** 1770 | * @ngdoc module 1771 | * @name ngMockE2E 1772 | * @module ngMockE2E 1773 | * @packageName angular-mocks 1774 | * @description 1775 | * 1776 | * The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing. 1777 | * Currently there is only one mock present in this module - 1778 | * the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock. 1779 | */ 1780 | angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { 1781 | $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); 1782 | }]); 1783 | 1784 | /** 1785 | * @ngdoc service 1786 | * @name $httpBackend 1787 | * @module ngMockE2E 1788 | * @description 1789 | * Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of 1790 | * applications that use the {@link ng.$http $http service}. 1791 | * 1792 | * *Note*: For fake http backend implementation suitable for unit testing please see 1793 | * {@link ngMock.$httpBackend unit-testing $httpBackend mock}. 1794 | * 1795 | * This implementation can be used to respond with static or dynamic responses via the `when` api 1796 | * and its shortcuts (`whenGET`, `whenPOST`, etc) and optionally pass through requests to the 1797 | * real $httpBackend for specific requests (e.g. to interact with certain remote apis or to fetch 1798 | * templates from a webserver). 1799 | * 1800 | * As opposed to unit-testing, in an end-to-end testing scenario or in scenario when an application 1801 | * is being developed with the real backend api replaced with a mock, it is often desirable for 1802 | * certain category of requests to bypass the mock and issue a real http request (e.g. to fetch 1803 | * templates or static files from the webserver). To configure the backend with this behavior 1804 | * use the `passThrough` request handler of `when` instead of `respond`. 1805 | * 1806 | * Additionally, we don't want to manually have to flush mocked out requests like we do during unit 1807 | * testing. For this reason the e2e $httpBackend flushes mocked out requests 1808 | * automatically, closely simulating the behavior of the XMLHttpRequest object. 1809 | * 1810 | * To setup the application to run with this http backend, you have to create a module that depends 1811 | * on the `ngMockE2E` and your application modules and defines the fake backend: 1812 | * 1813 | * ```js 1814 | * myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']); 1815 | * myAppDev.run(function($httpBackend) { 1816 | * phones = [{name: 'phone1'}, {name: 'phone2'}]; 1817 | * 1818 | * // returns the current list of phones 1819 | * $httpBackend.whenGET('/phones').respond(phones); 1820 | * 1821 | * // adds a new phone to the phones array 1822 | * $httpBackend.whenPOST('/phones').respond(function(method, url, data) { 1823 | * var phone = angular.fromJson(data); 1824 | * phones.push(phone); 1825 | * return [200, phone, {}]; 1826 | * }); 1827 | * $httpBackend.whenGET(/^\/templates\//).passThrough(); 1828 | * //... 1829 | * }); 1830 | * ``` 1831 | * 1832 | * Afterwards, bootstrap your app with this new module. 1833 | */ 1834 | 1835 | /** 1836 | * @ngdoc method 1837 | * @name $httpBackend#when 1838 | * @module ngMockE2E 1839 | * @description 1840 | * Creates a new backend definition. 1841 | * 1842 | * @param {string} method HTTP method. 1843 | * @param {string|RegExp} url HTTP url. 1844 | * @param {(string|RegExp)=} data HTTP request body. 1845 | * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header 1846 | * object and returns true if the headers match the current definition. 1847 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1848 | * control how a matched request is handled. 1849 | * 1850 | * - respond – 1851 | * `{function([status,] data[, headers, statusText]) 1852 | * | function(function(method, url, data, headers)}` 1853 | * – The respond method takes a set of static data to be returned or a function that can return 1854 | * an array containing response status (number), response data (string), response headers 1855 | * (Object), and the text for the status (string). 1856 | * - passThrough – `{function()}` – Any request matching a backend definition with 1857 | * `passThrough` handler will be passed through to the real backend (an XHR request will be made 1858 | * to the server.) 1859 | */ 1860 | 1861 | /** 1862 | * @ngdoc method 1863 | * @name $httpBackend#whenGET 1864 | * @module ngMockE2E 1865 | * @description 1866 | * Creates a new backend definition for GET requests. For more info see `when()`. 1867 | * 1868 | * @param {string|RegExp} url HTTP url. 1869 | * @param {(Object|function(Object))=} headers HTTP headers. 1870 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1871 | * control how a matched request is handled. 1872 | */ 1873 | 1874 | /** 1875 | * @ngdoc method 1876 | * @name $httpBackend#whenHEAD 1877 | * @module ngMockE2E 1878 | * @description 1879 | * Creates a new backend definition for HEAD requests. For more info see `when()`. 1880 | * 1881 | * @param {string|RegExp} url HTTP url. 1882 | * @param {(Object|function(Object))=} headers HTTP headers. 1883 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1884 | * control how a matched request is handled. 1885 | */ 1886 | 1887 | /** 1888 | * @ngdoc method 1889 | * @name $httpBackend#whenDELETE 1890 | * @module ngMockE2E 1891 | * @description 1892 | * Creates a new backend definition for DELETE requests. For more info see `when()`. 1893 | * 1894 | * @param {string|RegExp} url HTTP url. 1895 | * @param {(Object|function(Object))=} headers HTTP headers. 1896 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1897 | * control how a matched request is handled. 1898 | */ 1899 | 1900 | /** 1901 | * @ngdoc method 1902 | * @name $httpBackend#whenPOST 1903 | * @module ngMockE2E 1904 | * @description 1905 | * Creates a new backend definition for POST requests. For more info see `when()`. 1906 | * 1907 | * @param {string|RegExp} url HTTP url. 1908 | * @param {(string|RegExp)=} data HTTP request body. 1909 | * @param {(Object|function(Object))=} headers HTTP headers. 1910 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1911 | * control how a matched request is handled. 1912 | */ 1913 | 1914 | /** 1915 | * @ngdoc method 1916 | * @name $httpBackend#whenPUT 1917 | * @module ngMockE2E 1918 | * @description 1919 | * Creates a new backend definition for PUT requests. For more info see `when()`. 1920 | * 1921 | * @param {string|RegExp} url HTTP url. 1922 | * @param {(string|RegExp)=} data HTTP request body. 1923 | * @param {(Object|function(Object))=} headers HTTP headers. 1924 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1925 | * control how a matched request is handled. 1926 | */ 1927 | 1928 | /** 1929 | * @ngdoc method 1930 | * @name $httpBackend#whenPATCH 1931 | * @module ngMockE2E 1932 | * @description 1933 | * Creates a new backend definition for PATCH requests. For more info see `when()`. 1934 | * 1935 | * @param {string|RegExp} url HTTP url. 1936 | * @param {(string|RegExp)=} data HTTP request body. 1937 | * @param {(Object|function(Object))=} headers HTTP headers. 1938 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1939 | * control how a matched request is handled. 1940 | */ 1941 | 1942 | /** 1943 | * @ngdoc method 1944 | * @name $httpBackend#whenJSONP 1945 | * @module ngMockE2E 1946 | * @description 1947 | * Creates a new backend definition for JSONP requests. For more info see `when()`. 1948 | * 1949 | * @param {string|RegExp} url HTTP url. 1950 | * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that 1951 | * control how a matched request is handled. 1952 | */ 1953 | angular.mock.e2e = {}; 1954 | angular.mock.e2e.$httpBackendDecorator = 1955 | ['$rootScope', '$delegate', '$browser', createHttpBackendMock]; 1956 | 1957 | 1958 | angular.mock.clearDataCache = function() { 1959 | var key, 1960 | cache = angular.element.cache; 1961 | 1962 | for(key in cache) { 1963 | if (Object.prototype.hasOwnProperty.call(cache,key)) { 1964 | var handle = cache[key].handle; 1965 | 1966 | handle && angular.element(handle.elem).off(); 1967 | delete cache[key]; 1968 | } 1969 | } 1970 | }; 1971 | 1972 | 1973 | if(window.jasmine || window.mocha) { 1974 | 1975 | var currentSpec = null, 1976 | isSpecRunning = function() { 1977 | return !!currentSpec; 1978 | }; 1979 | 1980 | 1981 | (window.beforeEach || window.setup)(function() { 1982 | currentSpec = this; 1983 | }); 1984 | 1985 | (window.afterEach || window.teardown)(function() { 1986 | var injector = currentSpec.$injector; 1987 | 1988 | angular.forEach(currentSpec.$modules, function(module) { 1989 | if (module && module.$$hashKey) { 1990 | module.$$hashKey = undefined; 1991 | } 1992 | }); 1993 | 1994 | currentSpec.$injector = null; 1995 | currentSpec.$modules = null; 1996 | currentSpec = null; 1997 | 1998 | if (injector) { 1999 | injector.get('$rootElement').off(); 2000 | injector.get('$browser').pollFns.length = 0; 2001 | } 2002 | 2003 | angular.mock.clearDataCache(); 2004 | 2005 | // clean up jquery's fragment cache 2006 | angular.forEach(angular.element.fragments, function(val, key) { 2007 | delete angular.element.fragments[key]; 2008 | }); 2009 | 2010 | MockXhr.$$lastInstance = null; 2011 | 2012 | angular.forEach(angular.callbacks, function(val, key) { 2013 | delete angular.callbacks[key]; 2014 | }); 2015 | angular.callbacks.counter = 0; 2016 | }); 2017 | 2018 | /** 2019 | * @ngdoc function 2020 | * @name angular.mock.module 2021 | * @description 2022 | * 2023 | * *NOTE*: This function is also published on window for easy access.
2024 | * *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha 2025 | * 2026 | * This function registers a module configuration code. It collects the configuration information 2027 | * which will be used when the injector is created by {@link angular.mock.inject inject}. 2028 | * 2029 | * See {@link angular.mock.inject inject} for usage example 2030 | * 2031 | * @param {...(string|Function|Object)} fns any number of modules which are represented as string 2032 | * aliases or as anonymous module initialization functions. The modules are used to 2033 | * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. If an 2034 | * object literal is passed they will be registered as values in the module, the key being 2035 | * the module name and the value being what is returned. 2036 | */ 2037 | window.module = angular.mock.module = function() { 2038 | var moduleFns = Array.prototype.slice.call(arguments, 0); 2039 | return isSpecRunning() ? workFn() : workFn; 2040 | ///////////////////// 2041 | function workFn() { 2042 | if (currentSpec.$injector) { 2043 | throw new Error('Injector already created, can not register a module!'); 2044 | } else { 2045 | var modules = currentSpec.$modules || (currentSpec.$modules = []); 2046 | angular.forEach(moduleFns, function(module) { 2047 | if (angular.isObject(module) && !angular.isArray(module)) { 2048 | modules.push(function($provide) { 2049 | angular.forEach(module, function(value, key) { 2050 | $provide.value(key, value); 2051 | }); 2052 | }); 2053 | } else { 2054 | modules.push(module); 2055 | } 2056 | }); 2057 | } 2058 | } 2059 | }; 2060 | 2061 | /** 2062 | * @ngdoc function 2063 | * @name angular.mock.inject 2064 | * @description 2065 | * 2066 | * *NOTE*: This function is also published on window for easy access.
2067 | * *NOTE*: This function is declared ONLY WHEN running tests with jasmine or mocha 2068 | * 2069 | * The inject function wraps a function into an injectable function. The inject() creates new 2070 | * instance of {@link auto.$injector $injector} per test, which is then used for 2071 | * resolving references. 2072 | * 2073 | * 2074 | * ## Resolving References (Underscore Wrapping) 2075 | * Often, we would like to inject a reference once, in a `beforeEach()` block and reuse this 2076 | * in multiple `it()` clauses. To be able to do this we must assign the reference to a variable 2077 | * that is declared in the scope of the `describe()` block. Since we would, most likely, want 2078 | * the variable to have the same name of the reference we have a problem, since the parameter 2079 | * to the `inject()` function would hide the outer variable. 2080 | * 2081 | * To help with this, the injected parameters can, optionally, be enclosed with underscores. 2082 | * These are ignored by the injector when the reference name is resolved. 2083 | * 2084 | * For example, the parameter `_myService_` would be resolved as the reference `myService`. 2085 | * Since it is available in the function body as _myService_, we can then assign it to a variable 2086 | * defined in an outer scope. 2087 | * 2088 | * ``` 2089 | * // Defined out reference variable outside 2090 | * var myService; 2091 | * 2092 | * // Wrap the parameter in underscores 2093 | * beforeEach( inject( function(_myService_){ 2094 | * myService = _myService_; 2095 | * })); 2096 | * 2097 | * // Use myService in a series of tests. 2098 | * it('makes use of myService', function() { 2099 | * myService.doStuff(); 2100 | * }); 2101 | * 2102 | * ``` 2103 | * 2104 | * See also {@link angular.mock.module angular.mock.module} 2105 | * 2106 | * ## Example 2107 | * Example of what a typical jasmine tests looks like with the inject method. 2108 | * ```js 2109 | * 2110 | * angular.module('myApplicationModule', []) 2111 | * .value('mode', 'app') 2112 | * .value('version', 'v1.0.1'); 2113 | * 2114 | * 2115 | * describe('MyApp', function() { 2116 | * 2117 | * // You need to load modules that you want to test, 2118 | * // it loads only the "ng" module by default. 2119 | * beforeEach(module('myApplicationModule')); 2120 | * 2121 | * 2122 | * // inject() is used to inject arguments of all given functions 2123 | * it('should provide a version', inject(function(mode, version) { 2124 | * expect(version).toEqual('v1.0.1'); 2125 | * expect(mode).toEqual('app'); 2126 | * })); 2127 | * 2128 | * 2129 | * // The inject and module method can also be used inside of the it or beforeEach 2130 | * it('should override a version and test the new version is injected', function() { 2131 | * // module() takes functions or strings (module aliases) 2132 | * module(function($provide) { 2133 | * $provide.value('version', 'overridden'); // override version here 2134 | * }); 2135 | * 2136 | * inject(function(version) { 2137 | * expect(version).toEqual('overridden'); 2138 | * }); 2139 | * }); 2140 | * }); 2141 | * 2142 | * ``` 2143 | * 2144 | * @param {...Function} fns any number of functions which will be injected using the injector. 2145 | */ 2146 | 2147 | 2148 | 2149 | var ErrorAddingDeclarationLocationStack = function(e, errorForStack) { 2150 | this.message = e.message; 2151 | this.name = e.name; 2152 | if (e.line) this.line = e.line; 2153 | if (e.sourceId) this.sourceId = e.sourceId; 2154 | if (e.stack && errorForStack) 2155 | this.stack = e.stack + '\n' + errorForStack.stack; 2156 | if (e.stackArray) this.stackArray = e.stackArray; 2157 | }; 2158 | ErrorAddingDeclarationLocationStack.prototype.toString = Error.prototype.toString; 2159 | 2160 | window.inject = angular.mock.inject = function() { 2161 | var blockFns = Array.prototype.slice.call(arguments, 0); 2162 | var errorForStack = new Error('Declaration Location'); 2163 | return isSpecRunning() ? workFn.call(currentSpec) : workFn; 2164 | ///////////////////// 2165 | function workFn() { 2166 | var modules = currentSpec.$modules || []; 2167 | 2168 | modules.unshift('ngMock'); 2169 | modules.unshift('ng'); 2170 | var injector = currentSpec.$injector; 2171 | if (!injector) { 2172 | injector = currentSpec.$injector = angular.injector(modules); 2173 | } 2174 | for(var i = 0, ii = blockFns.length; i < ii; i++) { 2175 | try { 2176 | /* jshint -W040 *//* Jasmine explicitly provides a `this` object when calling functions */ 2177 | injector.invoke(blockFns[i] || angular.noop, this); 2178 | /* jshint +W040 */ 2179 | } catch (e) { 2180 | if (e.stack && errorForStack) { 2181 | throw new ErrorAddingDeclarationLocationStack(e, errorForStack); 2182 | } 2183 | throw e; 2184 | } finally { 2185 | errorForStack = null; 2186 | } 2187 | } 2188 | } 2189 | }; 2190 | } 2191 | 2192 | 2193 | })(window, window.angular); 2194 | -------------------------------------------------------------------------------- /test/spec/services/angular-apimock.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | describe('Service: apiMock', function () { 5 | 6 | // load the service's module 7 | beforeEach(module('apiMock')); 8 | 9 | // Hack (?) to get the provider so we can call .config() 10 | var apiMockProvider; 11 | beforeEach(module(function (_apiMockProvider_) { 12 | apiMockProvider = _apiMockProvider_; 13 | })); 14 | 15 | // instantiate services 16 | var httpInterceptor; 17 | var apiMock; 18 | var $location; 19 | var $http; 20 | var $httpBackend; 21 | var $log; 22 | var $rootScope; 23 | var $timeout; 24 | 25 | var defaultApiPath; 26 | var defaultMockPath; 27 | var defaultExpectMethod; 28 | var defaultExpectPath; 29 | var defaultRequest; 30 | 31 | beforeEach(inject(function (_httpInterceptor_, _apiMock_, _$location_, _$http_, _$httpBackend_, _$log_, _$rootScope_, _$timeout_) { 32 | httpInterceptor = _httpInterceptor_; 33 | apiMock = _apiMock_; 34 | $location = _$location_; 35 | $http = _$http_; 36 | $httpBackend = _$httpBackend_; 37 | $log = _$log_; 38 | $rootScope = _$rootScope_; 39 | $timeout = _$timeout_; 40 | 41 | defaultApiPath = '/api/pokemon'; 42 | defaultMockPath = '/mock_data/pokemon.get.json'; 43 | defaultExpectPath = defaultMockPath; 44 | defaultExpectMethod = 'GET'; 45 | defaultRequest = { 46 | url: defaultApiPath, 47 | method: defaultExpectMethod 48 | }; 49 | })); 50 | 51 | afterEach(function () { 52 | $timeout.verifyNoPendingTasks(); 53 | 54 | $httpBackend.verifyNoOutstandingExpectation(); // loops and $httpBackend.expect() doesn't seem to play nice 55 | $httpBackend.verifyNoOutstandingRequest(); 56 | }); 57 | 58 | 59 | /* This doesn't behave as when in the browser? 60 | it('should detect apimock param after hash', function () { 61 | $location.url('/#/view/?apimock=true'); 62 | expect(apiMock.isMocking()).to.be.true; 63 | }); */ 64 | 65 | /* Need to test with html5Mode turned on, but how? 66 | it('should detect apimock param after hash', inject(function($locationProvider) { 67 | $locationProvider.html5Mode(true); 68 | $location.url('/#/view/?apimock=true'); 69 | expect(apiMock.isMocking()).to.be.true; 70 | })); */ 71 | 72 | 73 | // TODO: Add test for $http config overrides. 74 | describe('httpInterceptor', function () { 75 | 76 | function setGlobalCommand(command) { 77 | $location.search('apiMock', command); 78 | } 79 | 80 | function unsetGlobalCommand() { 81 | setGlobalCommand(null); 82 | } 83 | 84 | function expectMockEnabled() { 85 | } 86 | 87 | function expectMockDisabled() { 88 | defaultExpectPath = defaultApiPath; 89 | } 90 | 91 | function expectHttpFailure(doneCb, failCb) { 92 | $httpBackend.expect(defaultExpectMethod, defaultExpectPath).respond(404); 93 | 94 | $http(defaultRequest) // TODO: Callbacks isn't the proper way to test $http. It also doesn't seem to test properly as we can switch expectHttpSuccess() and expectHttpFailure() without tests failing. 95 | .success(function () { 96 | fail(); 97 | failCb && failCb(); 98 | }) 99 | .error(function (data, status) { 100 | doneCb && doneCb(data, status); 101 | }); 102 | 103 | $rootScope.$digest(); 104 | $httpBackend.flush(); 105 | $timeout.flush(); 106 | } 107 | 108 | function expectHttpSuccess(doneCb, failCb) { 109 | $httpBackend.expect(defaultExpectMethod, defaultExpectPath).respond({}); 110 | $http(defaultRequest) // TODO: Callbacks isn't the proper way to test $http. It also doesn't seem to test properly as we can switch expectHttpSuccess() and expectHttpFailure() without tests failing. 111 | .success(function () { 112 | doneCb && doneCb(); 113 | }) 114 | .error(function () { 115 | fail(); 116 | failCb && failCb(); 117 | }); 118 | 119 | $rootScope.$digest(); 120 | $httpBackend.flush(); 121 | $timeout.flush(); 122 | } 123 | 124 | 125 | describe('URL flag', function () { 126 | 127 | describe('detection', function () { 128 | 129 | it('should detect parameter regardless of case on "apiMock". (http://server/?aPiMoCk=true)', function () { 130 | var value = true; 131 | 132 | // Define a valid query string. 133 | var keys = [ 134 | 'apimock', 135 | 'apiMock', 136 | 'APIMOCK', 137 | 'ApImOcK', 138 | 'ApiMock' 139 | ]; 140 | 141 | angular.forEach(keys, function (key) { 142 | // Set location with the query string. 143 | $location.search(key, value); 144 | 145 | // Test connection. 146 | expectMockEnabled(); 147 | expectHttpSuccess(); 148 | 149 | // Remove param tested from the location. 150 | $location.search(key, null); 151 | }); 152 | }); 153 | 154 | it('should detect HTTP verb command as string', function () { 155 | $location.url('/page?apiMock=200'); 156 | 157 | // Cannot use $httpBackend.expect() because HTTP status doesn't do a request 158 | $http(defaultRequest) 159 | .success(fail) 160 | .error(function (data, status) { 161 | expect(apiMock._countFallbacks()).toEqual(0); 162 | expect(status).toEqual(200); 163 | }); 164 | 165 | $rootScope.$digest(); 166 | $timeout.flush(); 167 | }); 168 | 169 | it('should detect HTTP verb command as number', function () { 170 | $location.search('apimock', 200); 171 | 172 | // Cannot use $httpBackend.expect() because HTTP status doesn't do a request 173 | $http(defaultRequest) 174 | .success(fail) 175 | .error(function (data, status) { 176 | expect(apiMock._countFallbacks()).toEqual(0); 177 | expect(status).toEqual(200); 178 | }); 179 | 180 | $rootScope.$digest(); 181 | $timeout.flush(); 182 | }); 183 | 184 | it('should detect in search queries', function () { 185 | $location.url('/page?apiMock=true'); 186 | 187 | expectMockEnabled(); 188 | expectHttpSuccess(); 189 | }); 190 | 191 | it('should be disabled and do regular call if no flag is present', function () { 192 | expectMockDisabled(); 193 | expectHttpSuccess(); 194 | }); 195 | 196 | it('should accept only global flag set', function () { 197 | $location.search('apiMock', true); 198 | 199 | expectMockEnabled(); 200 | expectHttpSuccess(); 201 | }); 202 | 203 | it('should accept only local flag set', function () { 204 | defaultRequest.apiMock = true; 205 | 206 | expectMockEnabled(); 207 | expectHttpSuccess(); 208 | }); 209 | 210 | it('should accept local flag overriding global flag', function () { 211 | $location.search('apiMock', false); 212 | defaultRequest.apiMock = true; 213 | 214 | expectMockEnabled(); 215 | expectHttpSuccess(); 216 | }); 217 | 218 | it('should work as usual if no flag is set', function () { 219 | expectMockDisabled(); 220 | expectHttpFailure(); 221 | }); 222 | }); 223 | 224 | describe('command', function () { 225 | 226 | describe('auto', function () { 227 | beforeEach(function () { 228 | setGlobalCommand('auto'); 229 | }); 230 | 231 | afterEach(function () { 232 | unsetGlobalCommand(); 233 | }); 234 | 235 | it('should automatically mock when request fails', function () { 236 | // First, it will try the API which will return a 404. 237 | $httpBackend.expect('GET', defaultApiPath).respond(404); 238 | 239 | $http(defaultRequest); 240 | $httpBackend.flush(); 241 | 242 | // Now that it failed it will try the mock data instead. 243 | $httpBackend.expect(defaultExpectMethod, '/mock_data/pokemon.get.json').respond({}); 244 | $timeout.flush(); 245 | $httpBackend.flush(); 246 | 247 | // The fallback list should be empty now. 248 | // TODO: It doesn't actually detect if no HTTP call was done. 249 | $timeout.flush(); 250 | expect(apiMock._countFallbacks()).toEqual(0); 251 | }); 252 | 253 | it('can\'t automatically mock request on failure if the URL is an invalid API url', function () { 254 | // Don't include override, but use an URL that doesn't pass the isApiPath test. 255 | defaultExpectPath = '/something/people/pokemon'; 256 | defaultRequest.url = defaultExpectPath; 257 | 258 | // Do a call, and expect it to fail. 259 | expectHttpFailure(); 260 | }); 261 | }); 262 | 263 | describe('HTTP status', function () { 264 | 265 | beforeEach(function () { 266 | setGlobalCommand(404); 267 | }); 268 | 269 | afterEach(function () { 270 | unsetGlobalCommand(); 271 | }); 272 | 273 | it('should return status', function () { 274 | var options = [ 275 | 200, 276 | 404, 277 | 500 278 | ]; 279 | 280 | angular.forEach(options, function (option) { 281 | defaultRequest.apiMock = option; 282 | 283 | // Cannot use $httpBackend.expect() because HTTP status doesn't do a request 284 | $http(defaultRequest) 285 | .success(fail) 286 | .error(function (data, status) { 287 | expect(apiMock._countFallbacks()).toEqual(0); 288 | expect(status).toEqual(option); 289 | }); 290 | 291 | $rootScope.$digest(); 292 | $timeout.flush(); 293 | }); 294 | }); 295 | 296 | it('should have basic header data in $http request status override', function () { 297 | // Cannot use $httpBackend.expect() because HTTP status doesn't do a request 298 | $http(defaultRequest) 299 | .success(fail) 300 | .error(function (data, status, headers) { 301 | expect(apiMock._countFallbacks()).toEqual(0); 302 | expect(headers).toExist; 303 | expect(headers['Content-Type']).toEqual('text/html; charset=utf-8'); 304 | expect(headers.Server).toEqual('Angular ApiMock'); 305 | }); 306 | 307 | $rootScope.$digest(); 308 | $timeout.flush(); 309 | }); 310 | }); 311 | 312 | describe('mock', function () { 313 | 314 | beforeEach(function () { 315 | setGlobalCommand(true); 316 | }); 317 | 318 | afterEach(function () { 319 | unsetGlobalCommand(); 320 | }); 321 | 322 | it('should ignore query objects in request URL (path has /?)', function () { 323 | defaultRequest.url = '/api/pokemon/?name=Pikachu'; 324 | 325 | expectHttpSuccess(); 326 | }); 327 | 328 | it('should ignore query objects in request URL (path has only ?)', function () { 329 | defaultRequest.url = '/api/pokemon?name=Pikachu'; 330 | 331 | expectHttpSuccess(); 332 | }); 333 | 334 | it('should ignore query objects in config.params', function () { 335 | defaultRequest.params = { 'name': 'Pikachu' }; 336 | 337 | expectHttpSuccess(); 338 | }); 339 | 340 | it('should ignore config.data', function () { 341 | defaultRequest.data = { 'name': 'Pikachu' }; 342 | 343 | expectHttpSuccess(); 344 | }); 345 | 346 | it('should mock calls with valid API path', function () { 347 | expectHttpSuccess(); 348 | }); 349 | 350 | it('should not mock calls with invalid API path (no /api/ in path)', function () { 351 | defaultExpectPath = '/something/pikachu'; 352 | defaultRequest.url = defaultExpectPath; 353 | 354 | expectHttpFailure(); 355 | }); 356 | 357 | it('should not mock calls with wrong API path (/api/ is not the beginning of path)', function () { 358 | defaultExpectPath = '/wrong/api/pikachu'; 359 | defaultRequest.url = defaultExpectPath; 360 | 361 | expectHttpFailure(); 362 | }); 363 | 364 | it('should correctly reroute for all HTTP verbs', function () { 365 | var verbs = [ 366 | 'GET', 367 | 'POST', 368 | 'DELETE', 369 | 'PUT' 370 | ]; 371 | 372 | angular.forEach(verbs, function (verb) { 373 | defaultExpectPath = '/mock_data/pokemon.' + verb.toLowerCase() + '.json'; 374 | defaultRequest.method = verb; 375 | 376 | expectHttpSuccess(); 377 | }); 378 | }); 379 | 380 | }); 381 | 382 | describe('off', function () { 383 | 384 | it('should not mock with falsy values', function () { 385 | // Define falsy values. 386 | var values = [ 387 | false, 388 | '', 389 | 0, 390 | NaN, 391 | undefined, 392 | null 393 | ]; 394 | 395 | angular.forEach(values, function (value) { 396 | defaultRequest.apiMock = value; 397 | expectMockDisabled(); 398 | 399 | expectHttpFailure(); 400 | }); 401 | 402 | }); 403 | 404 | }); 405 | }); 406 | 407 | }); 408 | 409 | describe('module config', function () { 410 | 411 | describe('disable option', function () { 412 | 413 | beforeEach(function () { 414 | apiMockProvider.config({disable: true}); 415 | }); 416 | 417 | afterEach(function () { 418 | apiMockProvider.config({disable: false}); 419 | }); 420 | 421 | it('should override config default mock', function () { 422 | apiMockProvider.config({defaultMock: true}); 423 | 424 | // Test connection. 425 | expectMockDisabled(); 426 | expectHttpFailure(); 427 | 428 | unsetGlobalCommand(); 429 | }); 430 | 431 | it('should override command mock', function () { 432 | setGlobalCommand(true); 433 | 434 | // Test connection. 435 | expectMockDisabled(); 436 | expectHttpFailure(); 437 | 438 | unsetGlobalCommand(); 439 | }); 440 | 441 | it('should override command auto', function () { 442 | setGlobalCommand('auto'); 443 | 444 | // Test connection. 445 | expectMockDisabled(); 446 | expectHttpFailure(); 447 | 448 | unsetGlobalCommand(); 449 | }); 450 | 451 | }); 452 | 453 | describe('default mock option', function () { 454 | beforeEach(function () { 455 | //apiMockProvider.config({defaultMock: true}); 456 | }); 457 | 458 | afterEach(function () { 459 | apiMockProvider.config({defaultMock: false}); 460 | unsetGlobalCommand(); 461 | }); 462 | 463 | it('should mock even without global or local flag', function () { 464 | apiMockProvider.config({defaultMock: true}); 465 | 466 | // Test connection. 467 | expectMockEnabled(); 468 | expectHttpSuccess(); 469 | }); 470 | 471 | it('should be overriden by global flag', function () { 472 | apiMockProvider.config({defaultMock: true}); 473 | setGlobalCommand(false); 474 | 475 | // Test connection. 476 | expectMockDisabled(); 477 | expectHttpFailure(); 478 | }); 479 | 480 | it('should be overriden by local flag', function () { 481 | apiMockProvider.config({defaultMock: true}); 482 | defaultRequest.apiMock = false; 483 | 484 | // Test connection. 485 | expectMockDisabled(); 486 | expectHttpFailure(); 487 | }); 488 | 489 | it('should not mock when set to false', function () { 490 | apiMockProvider.config({defaultMock: false}); 491 | 492 | // Test connection. 493 | expectMockDisabled(); 494 | expectHttpFailure(); 495 | }); 496 | }); 497 | 498 | describe('allow regexp for apiPath option instead of string', function () { 499 | 500 | beforeEach(function () { 501 | // apiMockProvider.config({apiPath: [/\/(aPI)/i]}); 502 | apiMockProvider.config({apiPath: /\/(aPi|UPI|APU)/i}); 503 | setGlobalCommand(true); 504 | }); 505 | 506 | afterEach(function () { 507 | apiMockProvider.config({apiPath: '/api'}); 508 | unsetGlobalCommand(); 509 | }); 510 | 511 | it('should redirect when match', function () { 512 | defaultRequest.url = '/api/pokemon'; 513 | defaultExpectPath = '/mock_data/pokemon.get.json'; 514 | expectHttpSuccess(); 515 | }); 516 | 517 | it('should redirect any match', function () { 518 | defaultRequest.url = '/UPI/pokemon'; 519 | defaultExpectPath = '/mock_data/pokemon.get.json'; 520 | expectHttpSuccess(); 521 | }); 522 | 523 | it('should NOT redirect not matched', function () { 524 | defaultRequest.url = '/EPI/picachu'; 525 | defaultExpectPath = '/EPI/picachu'; 526 | expectHttpFailure(); 527 | }); 528 | 529 | }); 530 | 531 | describe('allow strings array for apiPath option', function () { 532 | 533 | beforeEach(function () { 534 | apiMockProvider.config({apiPath: [ '/other/api', '/api', '/v2api' ]}); 535 | setGlobalCommand(true); 536 | }); 537 | 538 | afterEach(function () { 539 | apiMockProvider.config({apiPath: '/api'}); 540 | unsetGlobalCommand(); 541 | }); 542 | 543 | it('should redirect when match first in list', function () { 544 | defaultRequest.url = '/other/api/otherpikamon'; 545 | defaultExpectPath = '/mock_data/otherpikamon.get.json'; 546 | expectHttpSuccess(); 547 | }); 548 | 549 | it('should redirect when match second in list', function () { 550 | defaultRequest.url = '/api/pikamon'; 551 | defaultExpectPath = '/mock_data/pikamon.get.json'; 552 | expectHttpSuccess(); 553 | }); 554 | 555 | it('should redirect when match third in list as regexp', function () { 556 | defaultRequest.url = '/v2api/v2pikamon'; 557 | defaultExpectPath = '/mock_data/v2pikamon.get.json'; 558 | expectHttpSuccess(); 559 | }); 560 | 561 | it('should NOT redirect not matched', function () { 562 | defaultRequest.url = '/v9api/v9picachu'; 563 | defaultExpectPath = '/v9api/v9picachu'; 564 | expectHttpFailure(); 565 | }); 566 | 567 | }); 568 | 569 | describe('allow regexp array for apiPath option', function () { 570 | 571 | beforeEach(function () { 572 | apiMockProvider.config({apiPath: [ /\/other\/api/i, /\/api/i, /\/v(2|3|4)api/i ]}); 573 | setGlobalCommand(true); 574 | }); 575 | 576 | afterEach(function () { 577 | apiMockProvider.config({apiPath: '/api'}); 578 | unsetGlobalCommand(); 579 | }); 580 | 581 | it('should redirect when match first in list', function () { 582 | defaultRequest.url = '/other/api/otherpikamon'; 583 | defaultExpectPath = '/mock_data/otherpikamon.get.json'; 584 | expectHttpSuccess(); 585 | }); 586 | 587 | it('should redirect when match second in list', function () { 588 | defaultRequest.url = '/api/pikamon'; 589 | defaultExpectPath = '/mock_data/pikamon.get.json'; 590 | expectHttpSuccess(); 591 | }); 592 | 593 | 594 | it('should redirect when match third in list as regexp', function () { 595 | defaultRequest.url = '/v2api/v2pikamon'; 596 | defaultExpectPath = '/mock_data/v2pikamon.get.json'; 597 | expectHttpSuccess(); 598 | }); 599 | 600 | it('should redirect when match third again in list as regexp', function () { 601 | defaultRequest.url = '/v3api/v3pikamon'; 602 | defaultExpectPath = '/mock_data/v3pikamon.get.json'; 603 | expectHttpSuccess(); 604 | }); 605 | 606 | it('should NOT redirect not matched', function () { 607 | defaultRequest.url = '/v9api/v9picachu'; 608 | defaultExpectPath = '/v9api/v9picachu'; 609 | expectHttpFailure(); 610 | }); 611 | 612 | }); 613 | 614 | describe('enable query params', function () { 615 | 616 | beforeEach(function () { 617 | apiMockProvider.config({stripQueries: false}); 618 | setGlobalCommand(true); 619 | }); 620 | 621 | afterEach(function () { 622 | apiMockProvider.config({stripQueries: true}); 623 | unsetGlobalCommand(); 624 | }); 625 | 626 | it('should still redirect simple paths without query params', function () { 627 | expectHttpSuccess(); 628 | }); 629 | 630 | it('should still ignore config.data', function () { 631 | defaultRequest.data = { 632 | 'moves': [ 'Thunder Shock', 'Volt Tackle' ] 633 | }; 634 | 635 | expectHttpSuccess(); 636 | }); 637 | 638 | it('should NOT ignore query objects in request URL (path has /?)', function () { 639 | defaultRequest.url = '/api/pokemon/?name=pikachu'; 640 | defaultExpectPath = '/mock_data/pokemon/name=pikachu.get.json'; 641 | expectHttpSuccess(); 642 | }); 643 | 644 | it('should NOT ignore query objects in request URL (path has only ?)', function () { 645 | defaultRequest.url = '/api/pokemon?name=pikachu'; 646 | defaultExpectPath = '/mock_data/pokemon/name=pikachu.get.json'; 647 | expectHttpSuccess(); 648 | }); 649 | 650 | it('should NOT ignore query objects in config.params', function () { 651 | defaultRequest.url = '/api/pokemon'; 652 | defaultRequest.params = { 'name': 'pikachu', 'strength': 'electricity' }; 653 | defaultExpectPath = '/mock_data/pokemon/name=pikachu&strength=electricity.get.json'; 654 | expectHttpSuccess(); 655 | }); 656 | 657 | it('should sort query objects in request URL', function () { 658 | defaultRequest.url = '/api/pokemon?strength=electricity&name=pikachu'; 659 | defaultExpectPath = '/mock_data/pokemon/name=pikachu&strength=electricity.get.json'; 660 | expectHttpSuccess(); 661 | }); 662 | 663 | it('should sort query objects in alphabetical order', function () { 664 | defaultRequest.url = '/api/pokemon'; 665 | defaultRequest.params = { 'strength': 'electricity', 'name': 'pikachu' }; 666 | defaultExpectPath = '/mock_data/pokemon/name=pikachu&strength=electricity.get.json'; 667 | expectHttpSuccess(); 668 | }); 669 | 670 | it('should handle a mix of query objects and query params in url', function () { 671 | defaultRequest.url = '/api/pokemon?strength=electricity&hp=150'; 672 | defaultRequest.params = { 'name': 'pikachu' }; 673 | defaultExpectPath = '/mock_data/pokemon/hp=150&name=pikachu&strength=electricity.get.json'; 674 | expectHttpSuccess(); 675 | }); 676 | 677 | it('should encode characters in query params', function () { 678 | defaultRequest.url = '/api/pokemon?lang=sl&name=pikaču'; 679 | defaultExpectPath = '/mock_data/pokemon/lang=sl&name=pika%c4%8du.get.json'; 680 | expectHttpSuccess(); 681 | }); 682 | 683 | it('should lowercase characters in query params', function () { 684 | defaultRequest.url = '/api/pokemon?NAME=PIKACHU'; 685 | defaultExpectPath = '/mock_data/pokemon/name=pikachu.get.json'; 686 | expectHttpSuccess(); 687 | }); 688 | 689 | it('should serialize nested objects', function () { 690 | defaultRequest.url = '/api/pokemon'; 691 | defaultRequest.params = { 692 | 'movesAppearences': { 693 | 'Thunder Shock': 'Pokémon - I Choose You!', 694 | 'Volt Tackle': 'May\'s Egg-Cellent Adventure!' 695 | } 696 | }; 697 | defaultExpectPath = '/mock_data/pokemon/movesappearences%5bthunder+shock%5d=pok%c3%a9mon+-+i+choose+you!&movesappearences%5bvolt+tackle%5d=may\'s+egg-cellent+adventure!.get.json'; 698 | 699 | expectHttpSuccess(); 700 | }); 701 | 702 | it('should serialize nested arrays', function () { 703 | defaultRequest.url = '/api/pokemon'; 704 | defaultRequest.params = { 705 | 'moves': [ 'Thunder Shock', 'Volt Tackle' ] 706 | }; 707 | defaultExpectPath = '/mock_data/pokemon/moves%5b%5d=thunder+shock&moves%5b%5d=volt+tackle.get.json'; 708 | 709 | expectHttpSuccess(); 710 | }); 711 | 712 | it('should serialize objects nested inside arrays', function () { 713 | defaultRequest.url = '/api/pokemon'; 714 | defaultRequest.params = { 715 | 'moves': [ { 716 | 'Thunderbolt': { 717 | power: 95, 718 | type: 'Electric' 719 | }}, { 720 | 'Double Edge': { 721 | power: 120, 722 | type: 'Normal' 723 | } 724 | } ] 725 | }; 726 | defaultExpectPath = '/mock_data/pokemon/moves%5b0%5d%5bthunderbolt%5d%5bpower%5d=95&moves%5b0%5d%5bthunderbolt%5d%5btype%5d=electric&moves%5b1%5d%5bdouble+edge%5d%5bpower%5d=120&moves%5b1%5d%5bdouble+edge%5d%5btype%5d=normal.get.json'; 727 | 728 | expectHttpSuccess(); 729 | }); 730 | 731 | it('should handle empty value', function () { 732 | defaultRequest.url = '/api/pokemon?releaseDate'; 733 | defaultExpectPath = '/mock_data/pokemon/releasedate.get.json'; 734 | 735 | expectHttpSuccess(); 736 | }); 737 | 738 | it('should handle undefined value', function () { 739 | defaultRequest.url = '/api/pokemon'; 740 | defaultRequest.params = { 741 | 'releaseDate': undefined 742 | }; 743 | defaultExpectPath = '/mock_data/pokemon/releasedate.get.json'; 744 | 745 | expectHttpSuccess(); 746 | }); 747 | 748 | it('should serialize date type', function () { 749 | defaultRequest.url = '/api/pokemon'; 750 | defaultRequest.params = { 751 | 'releaseDate': new Date(Date.UTC(96, 1, 27, 1, 2, 3)) 752 | }; 753 | defaultExpectPath = '/mock_data/pokemon/releasedate=1996-02-27t01.02.03.000z.get.json'; 754 | 755 | expectHttpSuccess(); 756 | }); 757 | }); 758 | 759 | describe('delay option', function () { 760 | var delayMs = 500; 761 | 762 | beforeEach(function () { 763 | apiMockProvider.config({delay: delayMs}); 764 | setGlobalCommand(true); 765 | }); 766 | 767 | afterEach(function () { 768 | apiMockProvider.config({delay: 0}); 769 | unsetGlobalCommand(); 770 | }); 771 | 772 | it('should delay the request', function () { 773 | var didRun = false; 774 | 775 | // Test connection. 776 | $httpBackend.expect(defaultRequest.method, defaultExpectPath).respond({}); 777 | $http(defaultRequest).success(function () { 778 | didRun = true; 779 | }); 780 | 781 | // Flush connection. 782 | $httpBackend.flush(); 783 | 784 | // Don't flush $timeout completely. 785 | $timeout.flush(delayMs - 1); 786 | expect($timeout.verifyNoPendingTasks).toThrow(); 787 | $rootScope.$digest(); 788 | expect(didRun).toBe(false); 789 | 790 | // Now flush it completely. 791 | $timeout.flush(1); 792 | expect($timeout.verifyNoPendingTasks).not.toThrow(); 793 | 794 | // Make sure it actually ran. (TODO: too many tests here) 795 | $rootScope.$digest(); 796 | expect(didRun).toBe(true); 797 | }); 798 | }); 799 | }); 800 | 801 | describe('logging', function () { 802 | 803 | it('should log when command is true', function () { 804 | setGlobalCommand(true); 805 | 806 | expectHttpSuccess(function () { 807 | expect($log.info.logs[0][0]).toEqual('apiMock: rerouting ' + defaultApiPath + ' to ' + defaultMockPath); 808 | }); 809 | 810 | unsetGlobalCommand(); 811 | }); 812 | 813 | it('should log when command is auto', function () { 814 | setGlobalCommand('auto'); 815 | 816 | // First, it will try the API which will return a 404. 817 | $httpBackend.expect('GET', defaultApiPath).respond(404); 818 | $http(defaultRequest); 819 | $httpBackend.flush(); 820 | 821 | // Now that it failed it will try the mock data instead. 822 | $httpBackend.expect(defaultExpectMethod, '/mock_data/pokemon.get.json').respond({}); 823 | $timeout.flush(); 824 | $httpBackend.flush(); 825 | 826 | // Should have logged. 827 | $timeout.flush(); 828 | expect($log.info.logs[0][0]).toEqual('apiMock: recovering from failure at ' + defaultApiPath); 829 | 830 | unsetGlobalCommand(); 831 | }); 832 | 833 | it('should log when command is a HTTP status', function () { 834 | setGlobalCommand(404); 835 | 836 | // Don't do $httpBackend.expect() because the command doesn't do a real request. 837 | $http(defaultRequest) 838 | .error(function () { 839 | expect($log.info.logs[0][0]).toEqual('apiMock: mocking HTTP status to 404'); 840 | }) 841 | .success(fail); 842 | 843 | $rootScope.$digest(); 844 | $timeout.flush(); 845 | 846 | unsetGlobalCommand(); 847 | }); 848 | 849 | }); 850 | 851 | }); 852 | }); 853 | --------------------------------------------------------------------------------