├── .editorconfig ├── .gitignore ├── .jscsrc ├── .jshintrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bard-ngRouteTester.js ├── bard.js ├── bower.json ├── dist ├── bard-ngRouteTester.js ├── bard.js └── bard.min.js ├── gulp.config.js ├── gulpfile.js ├── index.html ├── package.js ├── package.json ├── snippets └── brackets-testing-snippets.yaml └── tests ├── bard.injector.spec.js └── bard.mockService.spec.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Visual Studio Project # 2 | ################### 3 | *.user 4 | *.gpState 5 | *.suo 6 | bin 7 | obj 8 | /packages 9 | 10 | # Ignore Node & Bower 11 | ################### 12 | node_modules 13 | /src/client/build 14 | /src/build 15 | bower_components 16 | /report 17 | **/test/coverage 18 | 19 | # mongo db 20 | ################### 21 | #Don't commit Mongo Database files 22 | *.lock 23 | *.0 24 | *.1 25 | *.ns 26 | journal 27 | 28 | # Ignore Web Storm # 29 | .idea 30 | 31 | # Compiled source # 32 | ################### 33 | *.com 34 | *.class 35 | *.dll 36 | *.exe 37 | *.o 38 | *.so 39 | 40 | # Packages # 41 | ############ 42 | # it's better to unpack these files and commit the raw source 43 | # git has its own built in compression methods 44 | *.7z 45 | *.dmg 46 | *.gz 47 | *.iso 48 | *.jar 49 | *.rar 50 | *.tar 51 | *.xap 52 | *.zip 53 | 54 | # Logs and databases # 55 | ###################### 56 | *.log 57 | *.sql 58 | *.sqlite 59 | # *.sdf 60 | *.mdf 61 | *.ldf 62 | 63 | # OS generated files # 64 | ###################### 65 | .DS_Store* 66 | ehthumbs.db 67 | Icon? 68 | Thumbs.db 69 | packages 70 | ~$*.pp* 71 | 72 | # Plato generated files # 73 | ###################### 74 | /report 75 | 76 | # Meteor # 77 | ##################### 78 | .versions 79 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "excludeFiles": ["node_modules/**", "bower_components/**"], 3 | 4 | "requireCurlyBraces": [ 5 | "if", 6 | "else", 7 | "for", 8 | "while", 9 | "do", 10 | "try", 11 | "catch" 12 | ], 13 | "requireOperatorBeforeLineBreak": true, 14 | "requireCamelCaseOrUpperCaseIdentifiers": true, 15 | "maximumLineLength": { 16 | "value": 100, 17 | "allowComments": true, 18 | "allowRegex": true 19 | }, 20 | "validateIndentation": 4, 21 | "validateQuoteMarks": "'", 22 | 23 | "disallowMultipleLineStrings": true, 24 | "disallowMixedSpacesAndTabs": true, 25 | "disallowTrailingWhitespace": true, 26 | "disallowSpaceAfterPrefixUnaryOperators": true, 27 | "disallowMultipleVarDecl": null, 28 | 29 | "requireSpaceAfterKeywords": [ 30 | "if", 31 | "else", 32 | "for", 33 | "while", 34 | "do", 35 | "switch", 36 | "return", 37 | "try", 38 | "catch" 39 | ], 40 | "requireSpaceBeforeBinaryOperators": [ 41 | "=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", 42 | "&=", "|=", "^=", "+=", 43 | 44 | "+", "-", "*", "/", "%", "<<", ">>", ">>>", "&", 45 | "|", "^", "&&", "||", "===", "==", ">=", 46 | "<=", "<", ">", "!=", "!==" 47 | ], 48 | "requireSpaceAfterBinaryOperators": true, 49 | "requireSpacesInConditionalExpression": true, 50 | "requireSpaceBeforeBlockStatements": true, 51 | "requireLineFeedAtFileEnd": true, 52 | "disallowSpacesInsideObjectBrackets": "all", 53 | "disallowSpacesInsideArrayBrackets": "all", 54 | "disallowSpacesInsideParentheses": true, 55 | 56 | "validateJSDoc": { 57 | "checkParamNames": true, 58 | "requireParamTypes": true 59 | }, 60 | 61 | "disallowMultipleLineBreaks": true, 62 | 63 | "disallowCommaBeforeLineBreak": null, 64 | "disallowDanglingUnderscores": null, 65 | "disallowEmptyBlocks": null, 66 | "disallowMultipleLineStrings": null, 67 | "disallowTrailingComma": null, 68 | "requireCommaBeforeLineBreak": null, 69 | "requireDotNotation": null, 70 | "requireMultipleVarDecl": null, 71 | "requireParenthesesAroundIIFE": true 72 | } 73 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "camelcase": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "es3": false, 7 | "forin": true, 8 | "freeze": true, 9 | "immed": true, 10 | "indent": 4, 11 | "latedef": "nofunc", 12 | "newcap": true, 13 | "noarg": true, 14 | "noempty": true, 15 | "nonbsp": true, 16 | "nonew": true, 17 | "plusplus": false, 18 | "quotmark": "single", 19 | "undef": true, 20 | "unused": false, 21 | "strict": false, 22 | "maxparams": 10, 23 | "maxdepth": 5, 24 | "maxstatements": 40, 25 | "maxcomplexity": 8, 26 | "maxlen": 120, 27 | 28 | "asi": false, 29 | "boss": false, 30 | "debug": false, 31 | "eqnull": true, 32 | "esnext": false, 33 | "evil": false, 34 | "expr": false, 35 | "funcscope": false, 36 | "globalstrict": false, 37 | "iterator": false, 38 | "lastsemic": false, 39 | "laxbreak": false, 40 | "laxcomma": false, 41 | "loopfunc": true, 42 | "maxerr": false, 43 | "moz": false, 44 | "multistr": false, 45 | "notypeof": false, 46 | "proto": false, 47 | "scripturl": false, 48 | "shadow": false, 49 | "sub": true, 50 | "supernew": false, 51 | "validthis": false, 52 | "noyield": false, 53 | 54 | "browser": true, 55 | "node": true, 56 | 57 | "globals": { 58 | "angular": false 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | bardjs Change Log 2 | =================== 3 | ### 0.1.10 4 | - no functional changes. 5 | - sinon moved its release file so we are now using bower to get sinon directly. which is better, anyway 6 | 7 | ### 0.1.8 8 | - no functional changes. 9 | - reversed 0.1.7. Apparently [peerDependecies are a horrible idea](https://github.com/npm/npm/issues/5080) and have been deprecated. It also seems that bardjs is DIRECTLY dependent on sinon so it's back to being a dependency. 10 | 11 | ### 0.1.7 12 | - no functional changes. 13 | - made sinon a [peerDependency](http://blog.nodejs.org/2013/02/07/peer-dependencies/) in npm package.json rather than a dependency. This changes means sinon is installed side-by-side bardjs (where you need it) rather than within bardjs's 14 | own node_modules folder. 15 | 16 | ### 0.1.6 17 | - no functional changes 18 | - updated package.json and bower.json descriptions to make clear that bardjs works w/ Jasmine and QUnit too 19 | - removed package.json install script that invoked bower ... which might not be installed by those who load bard with npm 20 | 21 | ### 0.1.5 22 | - no functional changes 23 | - added explanatory comments to $state and $route router fakes 24 | 25 | ### 0.1.4 26 | - updated dependency versioning 27 | 28 | ### 0.1.3 29 | - documentation about dependence on sinon 30 | - more robust handling of `this` when not using mocha; see [issue #5](https://github.com/wardbell/bardjs/issues/5). 31 | 32 | ### 0.1.2 33 | - handle services that have prototype methods/attributes; see 34 | [pr #4](https://github.com/wardbell/bardjs/pull/4). 35 | 36 | ### 0.1.1 37 | - incorporate `Function.bind` polyfill (for testing in phantom.js) 38 | 39 | ### 0.1.0 40 | - added brackets code snippets (draft) 41 | 42 | ### 0.0.9 43 | - added comments to make clear that `bard.appModule` should NOT be used if you'll be testing router services because it fakes their providers and that can't be reversed. Use regular `angular.mock.module` instead as directed. 44 | 45 | ### 0.0.8 46 | - bard.inject should work for QUnit too (removed mocha/jasmine limitation). 47 | - Need QUnit tests.### 0.0.6 48 | - heavily revamped bard.inject. added diagnostic bard.debug 49 | 50 | ### 0.0.7 51 | - bard.inject no longer uses evil Function; added addGlobals, mochaRunnerListener 52 | 53 | ### Coming Soon 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014, 2015 Ward Bell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bardjs Test Helpers 2 | [![NPM version](https://img.shields.io/npm/v/bardjs.svg?style=flat)](https://www.npmjs.com/package/bardjs) 3 | 4 | **bardjs** is a small library of functions to help you write **Angular v.1.x application tests** ... whether you write them in [mocha](http://mochajs.org/ "mochajs") or [jasmine](http://jasmine.github.io/ "jasmine") or [QUnit](http://qunitjs.com/ "QUnit"). 5 | 6 | What kind of help? Help with **routine tasks** that would otherwise clutter your tests and obscure their intent. 7 | 8 | The poster child in this respect is the [`inject` method](#inject). It can easily remove 10 or more lines of boilerplate so you spend less time with setup and more time with your tests. Check it out. 9 | 10 | The [bardjs repo](https://github.com/wardbell/bardjs/snippets/ "bard code snippets") also contains code snippets to make writing tests a little easier. See [separate instructions](#snippets) for those below. 11 | 12 | # Installation 13 | 14 | Most folks bardjs install it with [bower](http://bower.io/search/?q=bardjs "bard on bower") or [npm](https://www.npmjs.com/package/bardjs): 15 | 16 | `bower install bardjs` 17 | 18 | `npm install bardjs` 19 | 20 | You can also clone [bardjs from github](https://github.com/wardbell/bardjs "bard on github") and extract *bard.js*itself. 21 | 22 | >bard depends on [sinon.js](http://sinonjs.org/) so make sure you have that library available; bower and npm bring that down for you. 23 | 24 | Almost all of bard is in the *bard.js* file within the *dist* folder. 25 | 26 | If you're running tests in a browser, add the appropriate script tag *below* the script for your test framework library: 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | You'll need to add *sinon.js* as well 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ## karma considerations 43 | 44 | If you're running with [karma](http://karma-runner.github.io/0.12/index.html "karma"), reference *bard.js* in *karma.config.js* among the `files` to be loaded. See the [karma "Config" documentation](http://karma-runner.github.io/0.12/config/configuration-file.html "karma config") for details. 45 | 46 | Be sure to include *sinon* among the karma frameworks as in this example extract: 47 | 48 | frameworks: ['mocha', 'chai', 'sinon', 'chai-sinon'], 49 | 50 | In the *dist* folder you'll also find optional plug-in extensions such as the *bard-ngRouteTester.js* which adds the `bard.ngRouteTester` helper to manage tests of the [original Angular router](https://docs.angularjs.org/api/ngRoute/service/$route "Angular $route"). 51 | 52 | # bard methods 53 | 54 | After loading `bard.js`, you'll find the global variable `bard` at your finger tips as you write your tests. 55 | 56 | The bard methods are listed right at the top of the *bard.js* file. 57 | 58 | We won't describe every method here. Each method is prefaced in the code with it own documentation in comments describing both purpose and usage. 59 | 60 | But we will call out the methods that have proven most notable and useful: 61 | 62 | * [appModule](#appModule) - identify the application module to test and also disable certain routine services. 63 | * [asyncModule](#asyncModule) - enable async integration testing by restoring `$http` and `$q` while identifying the application module to test. 64 | * [inject](#inject) - inject angular and application components and store them by name on the global `window` object. 65 | * [fake services](#fakeServices) - register disabled services that you can spy on. 66 | * [log](#log) - writes messages to `console` when bard debugging is turned on. 67 | * [mockService](#mockService) - create a mock for any service with spies and return values for every service member. 68 | 69 | 70 | 71 | ## appModule 72 | 73 | **Identify the application module to test and also disable certain routine services.** 74 | 75 | You typically identify the application module that defines the component you want to test and its dependent services at the top of a test suite. You do this with the [`angular.mock.module` function](https://docs.angularjs.org/api/ngMock/function/angular.mock.module "mock module"). 76 | 77 | We found that we routinely disable certain services at the same time. 78 | 79 | For example, we don't want to see [**toastr**](https://github.com/CodeSeven/toastr "toastr") messages in our browser while our tests are running. We may need to assert that `toastr` was called in a particular way but we'd prefer to hide the toasts themselves. 80 | 81 | We also discovered that routing services can fire when the app module loads and trigger failures that have nothing to do with the subject of our tests. We just want routing to go away. 82 | 83 | The bard `appModule` method is a quick way to both identify the module to test and disable the *toastr* and routing services. This one line ... 84 | 85 | `beforeEach(bard.appModule('myModule'));` 86 | 87 | does the work of these seven ... 88 | 89 | beforeEach(angular.mock.module( 90 | 'myModule', 91 | bard.fakeToastr, 92 | bard.fakeRouteHelperProvider, 93 | bard.fakeRouteProvider, 94 | bard.fakeStateProvider) 95 | ); 96 | 97 | >The bard library offers [several methods](#fakeServices) (all beginning with the word "fake") that each disable a particular service. Don't worry if you haven't included all or any of these services in your app. Registering them will be harmless. Not using *toastr*? Not using the *UIRouter*? No problem. 98 | 99 | Like the [`angular.mock.module` function](https://docs.angularjs.org/api/ngMock/function/angular.mock.module "mock module"), you can add configuration arguments to the call to decorate or mock other services: 100 | 101 | `beforeEach(bard.appModule('myModule', someDecorator, someMock));` 102 | 103 | ### don't use *appModule* when testing routes 104 | 105 | You can't use `bard.appModule` and test the router. For example, if you want to know that a controller would route the user to a particular view, you can't use `bard.appModule`. There is no way to "unfake" the router service once it's been faked. 106 | 107 | Instead, simply fall back to `angular.mock.module`, adding specific fakes as desired: 108 | 109 | `beforeEach(module('myModule', bard.fakeToastr));` 110 | 111 | 112 | ## asyncModule 113 | 114 | **Enable async integration testing by restoring `$httpBackend` and `$q` while identifying the application module to test.** 115 | 116 | The [`angular.mock.module` function](https://docs.angularjs.org/api/ngMock/function/angular.mock.module "mock module") replaces `$httpBackend` and `$q` with mocked versions. 117 | 118 | The mocked `$httpBackend` prevents `$http` from issuing the AJAX calls necessary to communicate with a remote service. The mocked `$q` requires a manual digest cycle to "flush" the promise queue and prevents event-driven promise fulfillment. 119 | 120 | These mocks are great for testing *asynchronous* behaviors with fast *synchronous* tests. But you can't write integration tests that require interactions with a server while these mocks are in play. 121 | 122 | For example, you can't test that a `dataservice` works as expected when it sends requests to the remote data server. You can simulate how you *think* that server will respond. But what if the real server behaves differently than your simulation? How can you confirm that your `dataservice` continues working even after changes to the backend that you don't even know about? 123 | 124 | You'll want at least a few cross-process, ***truly asynchronous integration tests*** for peace of mind. You can't have them while `$httpBackend` and `$q` are mocked. 125 | 126 | The bard `asyncModule` method restores the original `$httpBackend` and `$q` at the same time that you identify the application module under test. Here's how you might call it: 127 | 128 | `beforeEach(bard.asyncModule('app'));` 129 | 130 | This is the equivalent of ... 131 | 132 | beforeEach(module('app', bard.$httpBackendReal, bard.$qReal, bard.fakeToastr)); 133 | 134 | >The bard library's `$httpBackendReal` and `$qReal` restore the original angular `$httpBackend` and `$q` implementations; they may be invoked independently. 135 | > 136 | >We're also faking *toastr* for the same reason we faked it in [`appModule`](#appModule). 137 | 138 | Now you write asynchronous tests that look a lot like production code. Here's a mocha example: 139 | 140 | it('should get at least 6 Avengers', function (done) { 141 | dataservice 142 | .getAvengers() 143 | .then(function(data) { 144 | expect(data).to.have.length.above(6); 145 | }) 146 | .then(done, done); 147 | }); 148 | 149 | You should see network traffic for the request and response to the "Avengers" query. The promise succeeds (or fails) in real time, without stimulus from `$rootScope.$apply()`. The test framework pauses and waits until the server responds (or timesout) and the final `then` invokes the test harness' `done` function. Only then will it proceed to the next test in the suite. 150 | 151 | Like the [`angular.mock.module` function](https://docs.angularjs.org/api/ngMock/function/angular.mock.module "mock module"), you can add configuration arguments to decorate or mock other services: 152 | 153 | `beforeEach(bard.asyncModule('app', someDecorator, someMock));` 154 | 155 | 156 | 157 | 158 | ## inject 159 | 160 | **Inject angular and application components and store them by name on the global `window` object.** 161 | 162 | The `bard.inject` method tells the Angular [mock dependency injector](https://docs.angularjs.org/api/ngMock/function/angular.mock.inject "mock inject") to inject components for the currently active test. 163 | 164 | Here's how you might use `inject` within a `beforeEach` to get five dependent services while testing an Angular controller: 165 | 166 | bard.inject(this, '$controller', '$log', '$q', '$rootScope', 'dataservice'); 167 | 168 | Now you can refer to these services by name in subsequent test functions as in these examples: 169 | 170 | var controller = $controller('Avengers'); 171 | sinon.stub(dataservice, 'getAvengers').returns($q.when(avengers)); 172 | $rootScope.$apply(); 173 | expect(dataservice.getAvengers).to.have.been.calledOnce; 174 | expect($log.error.logs[0]).to.match(/doomed/); 175 | 176 | Compare the simplicity of 177 | 178 | bard.inject(this, '$controller', '$log', '$q', '$rootScope', 'dataservice'); 179 | 180 | to the typical approach without bard: 181 | 182 | // declare local variables for use within subsequent test functions 183 | var $controller, $log, $q, $rootScope, dataservice; 184 | 185 | // inject the services using Angular "underscore wrapping" 186 | beforeEach(inject(function(_$controller_, _$log_, _$q_, _$rootScope_, _dataservice_) { 187 | // wire local vars to the injected services 188 | $controller = _$controller_; 189 | $log = _$log_; 190 | $q = _$q_; 191 | $rootScope = _$rootScope_; 192 | dataservice = _dataservice_; 193 | })); 194 | 195 | Which would you rather write? As importantly, which would you rather *read* ... on your way to the important business of the tests themselves? 196 | 197 | ### "but globals are bad" 198 | 199 | It's a terrible idea to toss variables into the global namespace *in production*. 200 | 201 | It's tactically smart, productive, and convenient to do so *in your tests*. Disagree? Do you write your test code with `beforeEach`, `it`, `expect`, and `module`? Or would you rather write with the annoyingly verbose equivalents: `mocha.beforeEach`, `jasmine.it`, `chai.expect` and `angular.mock.module`? 202 | 203 | the main risk of globals is ***cross-test pollution***, the risk that the values you set in one test will carry over to a later test. Fortunately, bard `inject` deletes these variables from the global namespace at the end of each test. Each new test gets a clean slate. 204 | 205 | ### what is `this`? 206 | 207 | Notice the *`this`* argument in 208 | 209 | bard.inject(this, '$controller', '$log', '$q', '$rootScope', 'dataservice'); 210 | 211 | Test frameworks set `this` to the test context (the spec context) object when the test runs. If we pass the context to `inject`, it can tell the test framework to ignore the new injected variables that it adds to the global namespace (the `window` object). 212 | 213 | Do you care? You will care if you fear that your application code is leaking variables to the global namespace. You might then configure the test framework to detect such leaks. 214 | 215 | For example, mocha has a "checkLeaks" configuration option that you can turn on like so: 216 | 217 | 221 | 222 | Thus enabled, mocha fails any test that adds variables to the global namespace between the time the test starts and when it finishes. 223 | 224 | That's a problem for `bard.inject` which ***always*** adds new variables to globals. We don't want the tests to fail because of `bard.inject`. 225 | 226 | Fortunately, `inject` can tell mocha to ignore the injected variables if we give it the spec context via `this`. 227 | 228 | >Internally `inject` calls another bard function, `addGlobals`. You should call this too if you deliberately extend globals yourself. 229 | 230 | **You don't have to pass `this` to `inject` if you aren't checking for global leaks.** You are free to omit it as in: 231 | 232 | bard.inject('$controller', '$log', '$q', '$rootScope', 'dataservice'); 233 | 234 | Of course you'll regret the omission later should you decide to turn on mocha's global leak checking. We think it's prudent to include `this` in your call. 235 | 236 | ### *inject* a function 237 | 238 | The [`angular.mock.inject`](https://docs.angularjs.org/api/ngMock/function/angular.mock.inject "mock inject") function can both retrieve injectable "services" and do things with them in the function body. 239 | 240 | The bard `inject` method accepts the same kind of function which may be useful if you want to inject and do work at the same time. For example: 241 | 242 | beforeEach(bard.inject(function($controller, $log, $q, $rootScope, dataservice) { 243 | ... do work .. 244 | })); 245 | 246 | After the function completes, bard `inject` promotes the injected services to global variables. 247 | 248 | 249 | 250 | 251 | ## fake services 252 | 253 | **Register disabled services that you can spy on.** 254 | 255 | Our applications often depend on certain specific services that we like to disable during most of our tests. 256 | 257 | Bard offers fake versions of these services. Their methods names begin with the word "fake" and include: 258 | 259 | fakeLogger 260 | fakeRouteHelperProvider 261 | fakeRouteProvider 262 | fakeStateProvider 263 | fakeToastr 264 | 265 | Look for details in *bard.js*. They all have two features in common: 266 | 267 | 1. they do nothing 268 | 1. their function members are stubbed with [sinon spies](http://sinonjs.org/docs/#spies "sinon spies") 269 | 270 | The spies allow a test to assert that one of the service methods was called in the expected manner. 271 | 272 | `expect(toastr.error).to.have.been.calledWith('uh oh!');` 273 | 274 | You typically register these faked services by including them among the arguments to the [`angular.mock.module` function](https://docs.angularjs.org/api/ngMock/function/angular.mock.module "mock module") or one of its bard substitutes: 275 | 276 | beforeEach(module('myMod', bard.fakeLogger)); 277 | beforeEach(appModule('myMod', bard.fakeLogger)); 278 | beforeEach(asyncModule('myMod', bard.fakeLogger)); 279 | 280 | 281 | 282 | 283 | ## log 284 | 285 | **The bard `log` method writes messages to `console` when bard debugging is turned on.** 286 | 287 | Our tests generally don't write to the console because the console is usually hidden when running tests in the browser or is crowded with other messages when running in karma. 288 | 289 | But it can be helpful to sprinkle a little console logging in our code when trying to understand and debug complex tests. 290 | 291 | it('should be good', function() { 292 | ... tricky stuff that might not work ... 293 | bard.log('we got the goods'); // conditional bard logging 294 | ... more tricky stuff ... 295 | expect(good).to.match(/good/); 296 | }); 297 | 298 | We may wish to leave such diagnostic logging behind ... inert for the most part but ready to go again in a future visit. We can turn conditional logging on with `bard.debugging(true)` and off again with `bard.debugging(false)`. When debugging is off, calls to `bard.log` do nothing. 299 | 300 | Some of bard's own methods call `bard.log`. 301 | 302 | 303 | 304 | 305 | ## mockService 306 | 307 | **Quickly create a mock for any service with spies and return values for every service member.** 308 | 309 | It can be painful to mock a dependency with a large API. Suppose, for example, that our app has a `dataservice` with 30 members. We want to test a particular controller that depends on this service. 310 | 311 | That controller might call *any* of the service methods, either during initialization or when subjected to test conditions. For this round of tests, we only care when it calls the `dataservice.getAvengers` method. 312 | 313 | No matter what the controller does, the `dataservice` must not dispatch requests to a server. It's obviously terrible if the controller calls a missing method and the mock blows up. We'll have to mock every `dataservice` member ... and remember to update it as the `dataservice` evolves. 314 | 315 | Such a mock `dataservice` is tedious to write by hand, especially when we don't care what most of the members do. The bard `mockService` makes writing this fake a lot easier. The entire setup could be as simple as: 316 | 317 | beforeEach(function() { 318 | 319 | bard.appModule('app.avengers'); 320 | bard.inject(this, '$controller', '$q', '$rootScope', 'dataservice'); 321 | 322 | 323 | bard.mockService(dataservice, { 324 | getAvengers: $q.when(avengers), 325 | _default: $q.when([]) 326 | }); 327 | 328 | 329 | controller = $controller('Avengers'); 330 | $rootScope.$apply(); 331 | }); 332 | 333 | The details of `mockService` configuration are described in *bard.js*. You'll find usage examples in the test coverage (look for *~/tests/bard.mockService.spec.js*). 334 | 335 | We trust you can see the core ideas in this example: 336 | 337 | * you give `mockService` an instance of the real `dataservice` to act as a template. 338 | * the `mockService` replaces every `dataservice` member with a fake implementation. 339 | * all methods are stubbed with [sinon spies](http://sinonjs.org/docs/#spies "sinon spies"). 340 | * you can supply return values (such as fulfilled promises) for *specific* methods. 341 | * you determine default return values for the remaining *unspecified* methods. 342 | 343 | In this case, we arranged for the `getAvengers` method to return a resolved promise with fake "avenger" objects. The other 29 methods return a resolved promise with an empty array. 344 | 345 | That's easier to write and read than a mock `dataservice` with thirty hand-coded stub methods. 346 | 347 | And here are two mocha/chai tests that could follow that setup: 348 | 349 | it('controller activation gets avengers', function() { 350 | controller.activate(); // calls `dataservice.getAvengers` 351 | $rootScope.$apply(); // flush pending promises 352 | 353 | expect(controller.avengers).to.have.length(avengers.length); // same number as mocked 354 | 355 | expect(dataservice.getAvengers).to.have.been.calledOnce; // it's a spy 356 | }); 357 | 358 | // Call one of the default mock methods which should return 359 | // a promise resolving to an empty array 360 | // Note that the controller would not have called this on its own 361 | it('can call fake `dataservice.getNews`', function() { 362 | 363 | dataservice.getNews().then(function(news) { 364 | expect(news).to.have.length(0); 365 | }); 366 | 367 | $rootScope.$apply(); // flush pending promises 368 | 369 | // verify that `getNews` is actually a spy 370 | expect(dataservice.getNews).to.have.been.calledOnce; 371 | }); 372 | 373 | 374 | # Brackets code snippets 375 | 376 | Code snippets make test authoring just a little easier. Here 377 | are instructions for loading our snippets into the [Brackets editor](http://brackets.io/ "Brackets editor"). 378 | 379 | - Open the Brackets Extension manager ( File > Extension manager ) 380 | - Install ['Brackets Snippets (by edc)'](https://github.com/chuyik/brackets-snippets) 381 | - Click the light bulb in Brackets' right gutter 382 | - Click `Settings` and then `Import` 383 | - Click `Choose File` 384 | - Locate and download [*~/snippets/brackets-testing-snippets.yaml*](https://github.com/wardbell/bardjs/blob/master/snippets/brackets-testing-snippets.yaml "bard brackets snippets on github") from github. 385 | - Choose either to `skip` or to `override` 386 | - Click `Start Import` 387 | 388 | Now try them in a JavaScript test file 389 | 390 | * mocha/jasmine 391 | 392 | * `bdescribe` - mocha/jasmine `describe` 393 | * `bit` - `it` test (synchronous) 394 | * `bait` - async `it` test 395 | * `bbeforeEach` - mocha/jasmine `beforeEach` 396 | * `bafterEach` - mocha/jasmine `afterEach` 397 | * `bdone` - tail of a mocha test promise chain: `.then(done, done);` 398 | 399 | 400 | * chai expectations 401 | 402 | * `bexpect` - expect(...).to 403 | * `bcalled` - expect(...).to.have.been.called 404 | * `bequal` - expect(...).to.equal(...) 405 | * `blen` - expect(...).to.have.length(...) 406 | * `bmatch` - expect(...).to.match(/.../i) 407 | * `bprop` - expect(...).to.have.been.property(..., ...) 408 | * `bthrow` - expect function to throw 409 | 410 | 411 | * bard.js 412 | 413 | * `binject` - bard.inject 414 | * `bcinject` - bard.inject for a controller 415 | * `bmodule` - bard.appModule 416 | * `basyncmod` - bard.asyncModule 417 | * `bverify` - bard.verifyNoOutstandingHttpRequests() 418 | 419 | 420 | * angular.js 421 | 422 | * `bapply` - $rootScope.$apply(); 423 | * `bwhen` - $httpBackend.when('get', {url}).respond({status}, {data}); 424 | * `bflush` - $httpBackend.flush(); 425 | 426 | * miscellaneous 427 | 428 | * `bfn` - generates a function stub 429 | -------------------------------------------------------------------------------- /bard-ngRouteTester.js: -------------------------------------------------------------------------------- 1 | /* jshint -W117, -W030 */ 2 | (function() { 3 | window.bard = window.bard || {}; 4 | /** 5 | * Creates the global ngRouteTester function 6 | * to help test ngRoute changes in the DOM 7 | * 8 | * Usage: 9 | * 10 | * beforeEach(function() { 11 | * module('app.module', ngRouteTester(options)); 12 | * ... other config ... 13 | * ... ready to roll; inject! ... 14 | * bard.inject('ngRouteTester', ...); 15 | * }); 16 | * 17 | * @function ngRouteTester 18 | * @param {Object} [opts] 19 | * @param {Object} [opts.document=document] The document node of the page 20 | * @param {Object} [opts.templateUrl] The template file for the HTML layout of the tester 21 | * @param {Object} [opts.template] The template string for the HTML layout of the tester 22 | * @param {Object} [opts.mockLocationPaths=true] Whether or not to fake the URL change in the browser address bar 23 | * 24 | * Thanks to Matias Niemelä and his ngMidwayTester from 25 | * which most of this code is lifted. 26 | * See http://www.yearofmoo.com/2013/01/full-spectrum-testing-with-angularjs-and-karma.html 27 | */ 28 | window.bard.ngRouteTester = function(opts) { 29 | 30 | ngRouteTester.$inject = ['$provide']; 31 | 32 | return ngRouteTester; 33 | /////////////////// 34 | 35 | function ngRouteTester($provide) { 36 | var options = { 37 | document: document 38 | }; 39 | 40 | angular.extend(options, opts); 41 | configure(); 42 | 43 | $provide.factory('ngRouteTester', tester); 44 | 45 | /////////////////////// 46 | var $rootElement, 47 | $timers = [], 48 | $viewContainer, 49 | $terminalElement, 50 | $viewCounter = 0, 51 | doc, 52 | noop = angular.noop; 53 | 54 | var viewSelector = 'ng-view, [ng-view], .ng-view, [x-ng-view], [data-ng-view]'; 55 | 56 | function configure() { 57 | 58 | doc = options.document; 59 | 60 | $rootElement = angular.element(doc.createElement('div')); 61 | $provide.value('$rootElement', $rootElement); 62 | 63 | var mockPaths = options.mockLocationPaths; 64 | if (mockPaths == null ? true : mockPaths) { 65 | $provide.decorator('$location', LocationDecorator); 66 | } 67 | 68 | if (options.templateUrl) { getTemplate(); } 69 | 70 | if (options.template) { 71 | $rootElement.html(options.template); 72 | var view = angular.element($rootElement[0].querySelector(viewSelector)); 73 | $viewContainer = view.parent(); 74 | } else { 75 | $viewContainer = angular.element('
'); 76 | $rootElement.append($viewContainer); 77 | } 78 | } 79 | 80 | LocationDecorator.$inject = ['$delegate', '$rootScope']; 81 | 82 | function LocationDecorator($delegate, $rootScope) { 83 | var _path = $delegate.path(); 84 | $delegate.path = function(path) { 85 | if (path) { 86 | // sometimes the broadcast triggers a new request for same path 87 | // added this conditional to mitigate risk of this infinite loop 88 | if (_path !== path) { 89 | _path = path; 90 | $rootScope.$broadcast('$locationChangeSuccess', path); 91 | } 92 | return this; 93 | } else { 94 | return _path; 95 | } 96 | }; 97 | return $delegate; 98 | } 99 | 100 | // get the template from the server synchronously 101 | function getTemplate() { 102 | var request = new XMLHttpRequest(); 103 | request.open('GET', options.templateUrl, false); 104 | request.send(null); 105 | 106 | if (request.status !== 200) { 107 | throw new Error('ngRouteTester: Unable to download template file'); 108 | } 109 | 110 | options.template = request.responseText; 111 | } 112 | 113 | // ngRouteTester factory 114 | tester.$inject = ['$compile', '$injector', '$rootScope', '$route']; 115 | 116 | function tester($compile, $injector, $rootScope, $route) { 117 | 118 | bootstrap(); 119 | 120 | // Arrange for mocha/jasmine to destroy after each test 121 | afterEach && afterEach(destroy); 122 | 123 | return { 124 | $injector: $injector, 125 | $rootScope: $rootScope, 126 | $route: $route, 127 | path: path, 128 | rootElement: $rootElement, 129 | until: until, 130 | viewElement : viewElement, 131 | visit : visit 132 | }; 133 | /////////////////// 134 | 135 | function bootstrap() { 136 | $terminalElement = angular.element( 137 | '
'); 138 | 139 | $rootElement.append($terminalElement); 140 | $rootScope.$apply(function() { 141 | $rootElement.data('$injector', $injector); 142 | $compile($rootElement)($rootScope); 143 | angular.element(doc.body).append($rootElement); 144 | }); 145 | } 146 | 147 | /** 148 | * Removes the $rootElement and clears the module from the page. 149 | * This is done automatically for mocha tests 150 | * 151 | * @method destroy 152 | */ 153 | function destroy() { 154 | angular.forEach($timers, function(timer) { 155 | clearTimeout(timer); 156 | }); 157 | 158 | var body = angular.element(document.body); 159 | body.removeData(); 160 | $rootElement.remove(); 161 | $rootScope.$destroy(); 162 | } 163 | 164 | /** 165 | * @method path 166 | * @return {String} Returns the path of the current route 167 | */ 168 | function path() { 169 | return $injector.get('$location').path(); 170 | } 171 | 172 | /** 173 | * @method viewElement 174 | * @return {Element} The current element that has ng-view attached to it 175 | */ 176 | function viewElement() { 177 | return angular.element($viewContainer[0].querySelector(viewSelector)); 178 | } 179 | 180 | /** 181 | * Changes the current route of the page and then fires the callback when the page has loaded 182 | * 183 | * @param {String} path The given path that the current route will be changed to 184 | * @param {function} [callback] The given callback to fire once the view has been fully loaded 185 | * @method visit 186 | */ 187 | function visit(path, callback) { 188 | 189 | // wait until view shows up 190 | /* jshint -W106 */ 191 | $rootScope.__VIEW_STATUS = ++$viewCounter; 192 | /* jshint +W106 */ 193 | until(function() { 194 | return parseInt($terminalElement.attr('status')) >= $viewCounter; 195 | }, function() { 196 | // give it another tick to settle 197 | setTimeout(callback || noop, 0); 198 | }); 199 | 200 | // tell router to visit the view 201 | var fn = function() { 202 | $injector.get('$location').path(path); 203 | }; 204 | $rootScope.$$phase ? fn() : $rootScope.$apply(fn); 205 | } 206 | 207 | /** 208 | * Keeps checking an expression until it returns a truthy value and then runs the provided callback 209 | * 210 | * @param {function} exp The given function to poll 211 | * @param {function} callback The given callback to fire once the exp function returns a truthy value 212 | * @method until 213 | */ 214 | function until(exp, callback) { 215 | var timer, delay = 50; 216 | timer = setInterval(function() { 217 | if (exp()) { 218 | clearTimeout(timer); 219 | callback(); 220 | } 221 | }, delay); 222 | $timers.push(timer); 223 | } 224 | } 225 | } 226 | }; 227 | 228 | })(); 229 | -------------------------------------------------------------------------------- /bard.js: -------------------------------------------------------------------------------- 1 | /*jshint -W079, -W117 */ 2 | (function() { 3 | 4 | var bard = { 5 | $httpBackend: $httpBackendReal, 6 | $q: $qReal, 7 | addGlobals: addGlobals, 8 | appModule: appModule, 9 | assertFail: assertFail, 10 | asyncModule: asyncModule, 11 | debugging: bardDebugging, 12 | fakeLogger: fakeLogger, 13 | fakeRouteHelperProvider: fakeRouteHelperProvider, 14 | fakeRouteProvider: fakeRouteProvider, 15 | fakeStateProvider: fakeStateProvider, 16 | fakeToastr: fakeToastr, 17 | inject: bardInject, 18 | log: bardLog, 19 | mochaRunnerListener: mochaRunnerListener, 20 | mockService: mockService, 21 | replaceAccentChars: replaceAccentChars, 22 | verifyNoOutstandingHttpRequests: verifyNoOutstandingHttpRequests, 23 | wrapWithDone: wrapWithDone 24 | }; 25 | 26 | var global = (function() { return this; })(); 27 | 28 | // mocha/jasmine/QUnit fns 29 | var afterEach = global.afterEach || global.teardown; 30 | var beforeEach = global.beforeEach || global.setup; 31 | 32 | var clearInject = []; 33 | var currentSpec = null; 34 | var debugging = false; 35 | var logCounter = 0; 36 | var okGlobals = []; 37 | 38 | addBindPolyfill(); 39 | 40 | beforeEach(function bardTopBeforeEach() { 41 | currentSpec = this; 42 | }); 43 | 44 | afterEach(function bardTopAfterEach() { 45 | currentSpec = null; 46 | bard.log('clearing injected globals: ' + clearInject); 47 | angular.forEach(clearInject, function(name) { 48 | delete global[name]; 49 | }); 50 | clearInject.length = 0; 51 | okGlobals.length = 0; 52 | }); 53 | 54 | global.bard = angular.extend(global.bard || {}, bard); 55 | 56 | //////////////////////// 57 | 58 | /*jshint -W101 */ 59 | /** 60 | * Replaces the ngMock'ed $httpBackend with the real one from ng thus 61 | * restoring the ability to issue AJAX calls to the backend with $http. 62 | * 63 | * Note that $q remains ngMocked so you must flush $http calls ($rootScope.$digest). 64 | * Use $rootScope.$apply() for this purpose. 65 | * 66 | * Could restore $q with $qReal in which case don't need to flush. 67 | * 68 | * Inspired by this StackOverflow answer: 69 | * http://stackoverflow.com/questions/20864764/e2e-mock-httpbackend-doesnt-actually-passthrough-for-me/26992327?iemail=1&noredirect=1#26992327 70 | * 71 | * Usage: 72 | * 73 | * var myService; 74 | * 75 | * beforeEach(module(bard.$httpBackend, 'app'); 76 | * 77 | * beforeEach(inject(function(_myService_) { 78 | * myService = _myService_; 79 | * })); 80 | * 81 | * it('should return valid data', function(done) { 82 | * myService.remoteCall() 83 | * .then(function(data) { 84 | * expect(data).toBeDefined(); 85 | * }) 86 | * .then(done, done); 87 | * 88 | * // because not using $qReal, must flush the $http and $q queues 89 | * $rootScope.$apply; 90 | * }); 91 | */ 92 | /*jshint +W101 */ 93 | function $httpBackendReal($provide) { 94 | $provide.provider('$httpBackend', function() { 95 | /*jshint validthis:true */ 96 | this.$get = function() { 97 | return angular.injector(['ng']).get('$httpBackend'); 98 | }; 99 | }); 100 | } 101 | 102 | /** 103 | * Replaces the ngMock'ed $q with the real one from ng thus 104 | * obviating the need to flush $http and $q queues 105 | * at the expense of ability to control $q timing. 106 | * 107 | * Usage: 108 | * 109 | * var myService; 110 | * 111 | * // Consider: beforeEach(bard.asyncModule('app')); 112 | * 113 | * beforeEach(module(bard.$q, bard.$httpBackend, 'app'); 114 | * 115 | * beforeEach(inject(function(_myService_) { 116 | * myService = _myService_; 117 | * })); 118 | * 119 | * it('should return valid data', function(done) { 120 | * myService.remoteCall() 121 | * .then(function(data) { 122 | * expect(data).toBeDefined(); 123 | * }) 124 | * .then(done, done); 125 | * 126 | * // not need to flush 127 | * }); 128 | */ 129 | function $qReal($provide) { 130 | $provide.provider('$q', function() { 131 | /*jshint validthis:true */ 132 | this.$get = function() { 133 | return angular.injector(['ng']).get('$q'); 134 | }; 135 | }); 136 | } 137 | /** 138 | * Add names of globals to list of OK globals for this mocha spec 139 | * NB: Call this method ONLY if you're using mocha! 140 | * NB: Turn off browser-sync else mocha detects the browser-sync globals 141 | * like ` ___browserSync___` 142 | * 143 | * usage: 144 | * addGlobals(this, 'foo'); // where `this` is the spec context 145 | * addGlobals(this, 'foo', bar); 146 | * addGlobals.bind(this)('foo', 'bar'); 147 | * addGlobals(ctx, ['foo', 'bar']) // where ctx is the spec context 148 | */ 149 | function addGlobals() { 150 | var args = Array.prototype.slice.call(arguments); 151 | var ctx = getCtxFromArgs.bind(this)(args); 152 | var globs = angular.isArray(args[0]) ? args[0] : args; 153 | angular.forEach(globs, function(g) { 154 | if (okGlobals.indexOf(g) === -1) { 155 | okGlobals.push(g); 156 | } 157 | }); 158 | // if a mocha test, add the ok globals to it 159 | ctx && ctx.test && ctx.test.globals && ctx.test.globals(okGlobals); 160 | } 161 | 162 | /** 163 | * Prepare ngMocked application feature module 164 | * along with faked toastr, routehelper, 165 | * and faked router services. 166 | * Especially useful for controller testing 167 | * Use it as you would the ngMocks#module method 168 | * 169 | * DO NOT USE IF YOU NEED THE REAL ROUTER SERVICES! 170 | * Fall back to `angular.mock.module(...)` or just `module(...)` 171 | * 172 | * Useage: 173 | * beforeEach(bard.appModule('app.avengers')); 174 | * 175 | * Equivalent to: 176 | * beforeEach(angular.mock.module( 177 | * 'app.avengers', 178 | * bard.fakeToastr, 179 | * bard.fakeRouteHelperProvider, 180 | * bard.fakeRouteProvider, 181 | * bard.fakeStateProvider) 182 | * ); 183 | */ 184 | function appModule() { 185 | var args = Array.prototype.slice.call(arguments, 0); 186 | args = args.concat(fakeRouteHelperProvider, fakeRouteProvider, 187 | fakeStateProvider, fakeToastr); 188 | return angular.mock.module.apply(angular.mock, args); 189 | } 190 | 191 | /** 192 | * Assert a failure in mocha, without condition 193 | * 194 | * Useage: 195 | * assertFail('you are hosed') 196 | * 197 | * Responds: 198 | * AssertionError: you are hosed 199 | * at Object.assertFail (..../test/lib/bard.js:153:15) 200 | * at Context. (.../....spec.js:329:15) 201 | * 202 | * OR JUST THROW the chai.AssertionError and treat this 203 | * as a reminder of how to do it. 204 | */ 205 | function assertFail(message) { 206 | throw new chai.AssertionError(message); 207 | } 208 | 209 | /** 210 | * Prepare ngMocked module definition that makes real $http and $q calls 211 | * Also adds fakeLogger to the end of the definition 212 | * Use it as you would the ngMocks#module method 213 | * 214 | * Useage: 215 | * beforeEach(bard.asyncModule('app')); 216 | * 217 | * Equivalent to: 218 | * beforeEach(module('app', bard.$httpBackend, bard.$q, bard.fakeToastr)); 219 | */ 220 | function asyncModule() { 221 | var args = Array.prototype.slice.call(arguments, 0); 222 | args = args.concat($httpBackendReal, $qReal, fakeToastr); 223 | // build and return the ngMocked test module 224 | return angular.mock.module.apply(angular.mock, args); 225 | } 226 | 227 | /** 228 | * get/set bard debugging flag 229 | */ 230 | function bardDebugging(x) { 231 | if (typeof x !== 'undefined') { debugging = !!x; } 232 | return debugging; 233 | } 234 | 235 | /** 236 | * Write to console if bard debugging flag is on 237 | */ 238 | function bardLog(msg) { 239 | if (debugging) { 240 | console.log('---bard (' + (logCounter += 1) + ') ' + msg); 241 | } 242 | } 243 | 244 | /** 245 | * inject selected services into the windows object during test 246 | * then remove them when test ends with an `afterEach`. 247 | * 248 | * spares us the repetition of creating common service vars and injecting them 249 | * 250 | * Option: the first argument may be the mocha spec context object (`this`) 251 | * It MUST be `this` if you what to check for mocha global leaks. 252 | * Do NOT supply `this` as the first arg if you're not running mocha specs. 253 | * 254 | * remaining inject arguments may take one of 3 forms : 255 | * 256 | * function - This fn will be passed to ngMocks.inject. 257 | * Annotations extracted after inject does its thing. 258 | * [strings] - same string array you'd use to set fn.$inject 259 | * (...string) - string arguments turned into a string array 260 | 261 | * 262 | * usage: 263 | * 264 | * bard.inject(this, ...); // `this` === the spec context 265 | * 266 | * bard.inject(this, '$log', 'dataservice'); 267 | * bard.inject(this, ['$log', 'dataservice']); 268 | * bard.inject(this, function($log, dataservice) { ... }); 269 | * 270 | */ 271 | function bardInject () { 272 | var args = Array.prototype.slice.call(arguments); 273 | var ctx = getCtxFromArgs.bind(this)(args); 274 | var first = args[0]; 275 | 276 | if (typeof first === 'function') { 277 | // use ngMocks.inject to execute the func in the arg 278 | angular.mock.inject(first); 279 | args = first.$inject; 280 | if (!args) { 281 | // unfortunately ngMocks.inject only prepares inject.$inject for us 282 | // if using strictDi as of v.1.3.8 283 | // therefore, apply its annotation extraction logic manually 284 | args = getinjectargs(first); 285 | } 286 | } 287 | else if (angular.isArray(first)) { 288 | args = first; // assume is an array of strings 289 | } 290 | // else assume all args are strings 291 | 292 | var $injector = currentSpec.$injector; 293 | if (!$injector) { 294 | angular.mock.inject(); // create the injector 295 | $injector = currentSpec.$injector; 296 | } 297 | 298 | var names = []; 299 | angular.forEach(args, function(name, ix) { 300 | 301 | if (typeof name !== 'string') { 302 | return; // WAT? Only strings allowed. Let's skip it and move on. 303 | } 304 | var value = $injector.get(name); 305 | if (value == null) { return; } 306 | 307 | var pathName = name.split('.'); 308 | 309 | if (pathName.length > 1) { 310 | // name is a path like 'block.foo'. Can't use as identifier 311 | // assume last segment should be identifier name, e.g. 'foo' 312 | name = pathName[pathName.length - 1]; 313 | // todo: tolerate component names that are invalid JS identifiers, e.g. 'burning man' 314 | } 315 | global[name] = value; 316 | clearInject.push(name); 317 | names.push(name); 318 | }); 319 | 320 | bard.addGlobals.bind(ctx)(names); 321 | } 322 | 323 | function fakeLogger($provide) { 324 | $provide.value('logger', sinon.stub({ 325 | info: function() {}, 326 | error: function() {}, 327 | warning: function() {}, 328 | success: function() {} 329 | })); 330 | } 331 | 332 | function fakeToastr($provide) { 333 | $provide.constant('toastr', sinon.stub({ 334 | info: function() {}, 335 | error: function() {}, 336 | warning: function() {}, 337 | success: function() {} 338 | })); 339 | } 340 | 341 | function fakeRouteHelperProvider($provide) { 342 | $provide.provider('routehelper', function() { 343 | /* jshint validthis:true */ 344 | this.config = { 345 | $routeProvider: undefined, 346 | docTitle: 'Testing' 347 | }; 348 | this.$get = function() { 349 | return { 350 | configureRoutes: sinon.stub(), 351 | getRoutes: sinon.stub().returns([]), 352 | routeCounts: { 353 | errors: 0, 354 | changes: 0 355 | } 356 | }; 357 | }; 358 | }); 359 | } 360 | 361 | function fakeRouteProvider($provide) { 362 | /** 363 | * Stub out the $routeProvider so we avoid 364 | * all routing calls, including the default route 365 | * which runs on every test otherwise. 366 | * Make sure this goes before the inject in the spec. 367 | * 368 | * Optionally set up the fake behavior in your tests by monkey patching 369 | * the faked $route router. For example: 370 | * 371 | * beforeEach(function() { 372 | * // get fake $route router service 373 | * bard.inject(this, '$route'); 374 | * 375 | * // plug in fake $route router values for this set of tests 376 | * $route.current = { ... fake values here ... }; 377 | * $route.routes = { ... fake values here ... }; 378 | * }) 379 | */ 380 | $provide.provider('$route', function() { 381 | /* jshint validthis:true */ 382 | this.when = sinon.stub(); 383 | this.otherwise = sinon.stub(); 384 | 385 | this.$get = function() { 386 | return { 387 | // current: {}, // fake before each test as needed 388 | // routes: {} // fake before each test as needed 389 | // more? You'll know when it fails :-) 390 | _faked: 'this is the faked $route service' 391 | }; 392 | }; 393 | }); 394 | } 395 | 396 | function fakeStateProvider($provide) { 397 | /** 398 | * Stub out the $stateProvider so we avoid 399 | * all routing calls, including the default state 400 | * which runs on every test otherwise. 401 | * Make sure this goes before the inject in the spec. 402 | * 403 | * Optionally set up the fake behavior in your tests by monkey patching 404 | * the faked $state router. For example: 405 | * 406 | * beforeEach(function() { 407 | * // get fake $state router service 408 | * bard.inject(this, '$state'); 409 | * 410 | * // plug in fake $state router values for this set of tests 411 | * $state.current = { ... fake values here ... }; 412 | * $state.state = { ... fake values here ... }; 413 | * }) 414 | */ 415 | $provide.provider('$state', function() { 416 | /* jshint validthis:true */ 417 | this.state = sinon.stub(); 418 | 419 | this.$get = function() { 420 | return { 421 | // current: {}, // fake before each test as needed 422 | // state: {} // fake before each test as needed 423 | // more? You'll know when it fails :-) 424 | _faked: 'this is the faked $state service' 425 | }; 426 | }; 427 | }); 428 | $provide.provider('$urlRouter', function() { 429 | /* jshint validthis:true */ 430 | this.otherwise = sinon.stub(); 431 | 432 | this.$get = function() { 433 | return { 434 | // current: {}, // fake before each test as needed 435 | // states: {} // fake before each test as needed 436 | // more? You'll know when it fails :-) 437 | _faked: 'this is the faked $urlRouter service' 438 | }; 439 | }; 440 | }); 441 | } 442 | 443 | /** 444 | * Get the spec context from parameters (if there) 445 | * or from `this` (if it is the ctx as a result of `bind`) 446 | */ 447 | function getCtxFromArgs(args) { 448 | var ctx; 449 | var first = args[0]; 450 | // heuristic to discover if the first arg is the mocha spec context (`this`) 451 | if (first && first.test) { 452 | // The first arg was the mocha spec context (`this`) 453 | // Get it and strip it from args 454 | ctx = args.shift(); 455 | } else if (this.test) { 456 | // alternative: caller can bind bardInject to the spec context 457 | ctx = this; 458 | } 459 | return ctx; 460 | } 461 | 462 | /** 463 | * Inspired by Angular; that's how they get the parms for injection 464 | * Todo: no longer used by `injector`. Remove? 465 | */ 466 | function getFnParams(fn) { 467 | var fnText; 468 | var argDecl; 469 | 470 | var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; 471 | var FN_ARG_SPLIT = /,/; 472 | var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; 473 | var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; 474 | var params = []; 475 | if (fn.length) { 476 | fnText = fn.toString().replace(STRIP_COMMENTS, ''); 477 | argDecl = fnText.match(FN_ARGS); 478 | angular.forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) { 479 | arg.replace(FN_ARG, function(all, underscore, name) { 480 | params.push(name); 481 | }); 482 | }); 483 | } 484 | return params; 485 | } 486 | 487 | function isSpecRunning() { return !!currentSpec; } 488 | 489 | /** 490 | * Mocks out a service with sinon stubbed functions 491 | * that return the values specified in the config 492 | * 493 | * If the config value is `undefined`, 494 | * stub the service method with a dummy that doesn't return a value 495 | * 496 | * If the config value is a function, set service property with it 497 | * 498 | * If a service member is a property, not a function, 499 | * set it with the config value 500 | 501 | * If a service member name is not a key in config, 502 | * follow the same logic as above to set its members 503 | * using the config._default value (which is `undefined` if omitted) 504 | * 505 | * If there is a config entry that is NOT a member of the service 506 | * add mocked function to the service using the config value 507 | * 508 | * Usage: 509 | * Given this DoWork service: 510 | * { 511 | * doWork1: an async function, 512 | * doWork2: a function, 513 | * doWork3: an async function, 514 | * doWork4: a function, 515 | * isActive: true 516 | * } 517 | * 518 | * Given this config: 519 | * { 520 | * doWork1: $q.when([{name: 'Bob'}, {name: 'Sally'}]), 521 | * doWork2: undefined, 522 | * //doWork3: not in config therefore will get _default value 523 | * doWork4: an alternate doWork4 function 524 | * doWork5: $q.reject('bad boy!') 525 | * isActive: false, 526 | * _default: $q.when([]) 527 | * } 528 | * 529 | * Service becomes 530 | * { 531 | * doWork1: a stub returning $q.when([{name: 'Bob'}, {name: 'Sally'}]), 532 | * doWork2: do-nothing stub, 533 | * doWork3: a stub returning $q.when([]), 534 | * doWork4: an alternate doWork4 function, 535 | * doWork5: a stub returning $q.reject('bad boy!'), 536 | * isActive: false, 537 | * } 538 | */ 539 | function mockService(service, config) { 540 | 541 | var serviceKeys = []; 542 | for (var key in service) { 543 | serviceKeys.push(key); 544 | } 545 | 546 | var configKeys = []; 547 | for (var key in config) { 548 | configKeys.push(key); 549 | } 550 | 551 | angular.forEach(serviceKeys, function(key) { 552 | var value = configKeys.indexOf(key) > -1 ? 553 | config[key] : config._default; 554 | 555 | if (typeof service[key] === 'function') { 556 | if (typeof value === 'function') { 557 | sinon.stub(service, key, value); 558 | } else { 559 | sinon.stub(service, key, function() { 560 | return value; 561 | }); 562 | } 563 | } else { 564 | service[key] = value; 565 | } 566 | }); 567 | 568 | // for all unused config entries add a sinon stubbed 569 | // async method that returns the config value 570 | angular.forEach(configKeys, function(key) { 571 | if (serviceKeys.indexOf(key) === -1) { 572 | var value = config[key]; 573 | if (typeof value === 'function') { 574 | service[key] = value; 575 | } else { 576 | service[key] = sinon.spy(function() { 577 | return value; 578 | }); 579 | } 580 | } 581 | }); 582 | 583 | return service; 584 | } 585 | 586 | /** 587 | * Listen to mocha test runner events 588 | * Usage in browser: 589 | * var runner = mocha.run(); 590 | * bard.mochaRunnerListener(runner); 591 | */ 592 | function mochaRunnerListener(runner) { 593 | if (!global.mocha) { return; } 594 | if (!runner.ignoreLeaks) { 595 | runner.on('hook end', addOkGlobals); 596 | }; 597 | 598 | // When checking global leaks with mocha.checkLeaks() 599 | // make sure mocha is aware of bard's okGlobals 600 | function addOkGlobals(hook) { 601 | // HACK: only way I've found so far to ensure that bard added globals 602 | // are always inspected. Using private mocha _allowedGlobals (shhhh!) 603 | if (okGlobals.length && !hook._allowedGlobals) { 604 | hook._allowedGlobals = okGlobals; 605 | } 606 | } 607 | } 608 | 609 | // Replaces the accented characters of many European languages w/ unaccented chars 610 | // Use it in JavaScript string sorts where such characters may be encountered 611 | // Matches the default string comparers of most databases. 612 | // Ex: replaceAccentChars(a.Name) < replaceAccentChars(b.Name) 613 | // instead of: a.Name < b.Name 614 | function replaceAccentChars(s) { 615 | var r = s.toLowerCase(); 616 | r = r.replace(new RegExp(/[àáâãäå]/g), 'a'); 617 | r = r.replace(new RegExp(/æ/g), 'ae'); 618 | r = r.replace(new RegExp(/ç/g), 'c'); 619 | r = r.replace(new RegExp(/[èéêë]/g), 'e'); 620 | r = r.replace(new RegExp(/[ìíîï]/g), 'i'); 621 | r = r.replace(new RegExp(/ñ/g), 'n'); 622 | r = r.replace(new RegExp(/[òóôõö]/g), 'o'); 623 | r = r.replace(new RegExp(/œ/g), 'oe'); 624 | r = r.replace(new RegExp(/[ùúûü]/g), 'u'); 625 | r = r.replace(new RegExp(/[ýÿ]/g), 'y'); 626 | return r; 627 | } 628 | 629 | /** 630 | * Assert that there are no outstanding HTTP requests after test is complete 631 | * For use with ngMocks; doesn't work for async server integration tests 632 | */ 633 | function verifyNoOutstandingHttpRequests () { 634 | afterEach(angular.mock.inject(function($httpBackend) { 635 | $httpBackend.verifyNoOutstandingExpectation(); 636 | $httpBackend.verifyNoOutstandingRequest(); 637 | })); 638 | } 639 | 640 | /** 641 | * Returns a function that execute a callback function 642 | * (typically a fn making asserts) within a try/catch 643 | * The try/catch then calls the ambient "done" function 644 | * in the appropriate way for both success and failure 645 | * 646 | * Useage: 647 | * bard.inject('ngRouteTester', ...); // see bard-ngRouteTester.js 648 | * ... 649 | * // When the DOM is ready, assert got the dashboard view 650 | * ngRouteTester.until(elemIsReady, wrap(hasDashboardView, done)); 651 | */ 652 | function wrapWithDone(callback, done) { 653 | return function() { 654 | try { 655 | callback(); 656 | done(); 657 | } catch (err) { 658 | done(err); 659 | } 660 | }; 661 | } 662 | 663 | /* 664 | * Phantom.js does not support Function.prototype.bind (at least not before v.2.0 665 | * That's just crazy. Everybody supports bind. 666 | * Read about it here: https://groups.google.com/forum/#!msg/phantomjs/r0hPOmnCUpc/uxusqsl2LNoJ 667 | * This polyfill is copied directly from MDN 668 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#Compatibility 669 | */ 670 | function addBindPolyfill() { 671 | if (Function.prototype.bind) { return; } // already defined 672 | 673 | /*jshint freeze: false */ 674 | Function.prototype.bind = function (oThis) { 675 | if (typeof this !== 'function') { 676 | // closest thing possible to the ECMAScript 5 677 | // internal IsCallable function 678 | throw new TypeError( 679 | 'Function.prototype.bind - what is trying to be bound is not callable'); 680 | } 681 | 682 | var aArgs = Array.prototype.slice.call(arguments, 1), 683 | fToBind = this, 684 | FuncNoOp = function () {}, 685 | fBound = function () { 686 | return fToBind.apply(this instanceof FuncNoOp && oThis ? this : oThis, 687 | aArgs.concat(Array.prototype.slice.call(arguments))); 688 | }; 689 | 690 | FuncNoOp.prototype = this.prototype; 691 | fBound.prototype = new FuncNoOp(); 692 | 693 | return fBound; 694 | }; 695 | } 696 | 697 | })(); 698 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bardjs", 3 | "version": "0.1.10", 4 | "description": "Spec helpers for testing angular v.1.x apps with Mocha, Jasmine or QUnit", 5 | "authors": [ 6 | "John Papa", 7 | "Ward Bell" 8 | ], 9 | "main": [ 10 | "./dist/bard.js", 11 | "./dist/bard-ngRouteTester.js" 12 | ], 13 | "license": "MIT", 14 | "homepage": "https://github.com/wardbell/bardjs", 15 | "ignore": [ 16 | "node_modules", 17 | "bower_components", 18 | "tests", 19 | "*.html", 20 | "*.js", 21 | ".*" 22 | ], 23 | "dependencies": { 24 | "angular": ">=1.3.8", 25 | "angular-mocks": ">=1.3.8", 26 | "sinon": "~1.15.0" 27 | }, 28 | "devDependencies": { 29 | "chai": "^1.9.1", 30 | "sinon-chai": "^2.5.0" 31 | }, 32 | "exportsOverride": { 33 | "sinon": { 34 | "js": "index.js" 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /dist/bard-ngRouteTester.js: -------------------------------------------------------------------------------- 1 | /** 2 | * bardjs - Spec helpers for testing angular v.1.x apps with Mocha, Jasmine or QUnit 3 | * @authors John Papa,Ward Bell 4 | * @version v0.1.10 5 | * @link https://github.com/wardbell/bardjs 6 | * @license MIT 7 | */ 8 | /* jshint -W117, -W030 */ 9 | (function() { 10 | window.bard = window.bard || {}; 11 | /** 12 | * Creates the global ngRouteTester function 13 | * to help test ngRoute changes in the DOM 14 | * 15 | * Usage: 16 | * 17 | * beforeEach(function() { 18 | * module('app.module', ngRouteTester(options)); 19 | * ... other config ... 20 | * ... ready to roll; inject! ... 21 | * bard.inject('ngRouteTester', ...); 22 | * }); 23 | * 24 | * @function ngRouteTester 25 | * @param {Object} [opts] 26 | * @param {Object} [opts.document=document] The document node of the page 27 | * @param {Object} [opts.templateUrl] The template file for the HTML layout of the tester 28 | * @param {Object} [opts.template] The template string for the HTML layout of the tester 29 | * @param {Object} [opts.mockLocationPaths=true] Whether or not to fake the URL change in the browser address bar 30 | * 31 | * Thanks to Matias Niemelä and his ngMidwayTester from 32 | * which most of this code is lifted. 33 | * See http://www.yearofmoo.com/2013/01/full-spectrum-testing-with-angularjs-and-karma.html 34 | */ 35 | window.bard.ngRouteTester = function(opts) { 36 | 37 | ngRouteTester.$inject = ['$provide']; 38 | 39 | return ngRouteTester; 40 | /////////////////// 41 | 42 | function ngRouteTester($provide) { 43 | var options = { 44 | document: document 45 | }; 46 | 47 | angular.extend(options, opts); 48 | configure(); 49 | 50 | $provide.factory('ngRouteTester', tester); 51 | 52 | /////////////////////// 53 | var $rootElement, 54 | $timers = [], 55 | $viewContainer, 56 | $terminalElement, 57 | $viewCounter = 0, 58 | doc, 59 | noop = angular.noop; 60 | 61 | var viewSelector = 'ng-view, [ng-view], .ng-view, [x-ng-view], [data-ng-view]'; 62 | 63 | function configure() { 64 | 65 | doc = options.document; 66 | 67 | $rootElement = angular.element(doc.createElement('div')); 68 | $provide.value('$rootElement', $rootElement); 69 | 70 | var mockPaths = options.mockLocationPaths; 71 | if (mockPaths == null ? true : mockPaths) { 72 | $provide.decorator('$location', LocationDecorator); 73 | } 74 | 75 | if (options.templateUrl) { getTemplate(); } 76 | 77 | if (options.template) { 78 | $rootElement.html(options.template); 79 | var view = angular.element($rootElement[0].querySelector(viewSelector)); 80 | $viewContainer = view.parent(); 81 | } else { 82 | $viewContainer = angular.element('
'); 83 | $rootElement.append($viewContainer); 84 | } 85 | } 86 | 87 | LocationDecorator.$inject = ['$delegate', '$rootScope']; 88 | 89 | function LocationDecorator($delegate, $rootScope) { 90 | var _path = $delegate.path(); 91 | $delegate.path = function(path) { 92 | if (path) { 93 | // sometimes the broadcast triggers a new request for same path 94 | // added this conditional to mitigate risk of this infinite loop 95 | if (_path !== path) { 96 | _path = path; 97 | $rootScope.$broadcast('$locationChangeSuccess', path); 98 | } 99 | return this; 100 | } else { 101 | return _path; 102 | } 103 | }; 104 | return $delegate; 105 | } 106 | 107 | // get the template from the server synchronously 108 | function getTemplate() { 109 | var request = new XMLHttpRequest(); 110 | request.open('GET', options.templateUrl, false); 111 | request.send(null); 112 | 113 | if (request.status !== 200) { 114 | throw new Error('ngRouteTester: Unable to download template file'); 115 | } 116 | 117 | options.template = request.responseText; 118 | } 119 | 120 | // ngRouteTester factory 121 | tester.$inject = ['$compile', '$injector', '$rootScope', '$route']; 122 | 123 | function tester($compile, $injector, $rootScope, $route) { 124 | 125 | bootstrap(); 126 | 127 | // Arrange for mocha/jasmine to destroy after each test 128 | afterEach && afterEach(destroy); 129 | 130 | return { 131 | $injector: $injector, 132 | $rootScope: $rootScope, 133 | $route: $route, 134 | path: path, 135 | rootElement: $rootElement, 136 | until: until, 137 | viewElement : viewElement, 138 | visit : visit 139 | }; 140 | /////////////////// 141 | 142 | function bootstrap() { 143 | $terminalElement = angular.element( 144 | '
'); 145 | 146 | $rootElement.append($terminalElement); 147 | $rootScope.$apply(function() { 148 | $rootElement.data('$injector', $injector); 149 | $compile($rootElement)($rootScope); 150 | angular.element(doc.body).append($rootElement); 151 | }); 152 | } 153 | 154 | /** 155 | * Removes the $rootElement and clears the module from the page. 156 | * This is done automatically for mocha tests 157 | * 158 | * @method destroy 159 | */ 160 | function destroy() { 161 | angular.forEach($timers, function(timer) { 162 | clearTimeout(timer); 163 | }); 164 | 165 | var body = angular.element(document.body); 166 | body.removeData(); 167 | $rootElement.remove(); 168 | $rootScope.$destroy(); 169 | } 170 | 171 | /** 172 | * @method path 173 | * @return {String} Returns the path of the current route 174 | */ 175 | function path() { 176 | return $injector.get('$location').path(); 177 | } 178 | 179 | /** 180 | * @method viewElement 181 | * @return {Element} The current element that has ng-view attached to it 182 | */ 183 | function viewElement() { 184 | return angular.element($viewContainer[0].querySelector(viewSelector)); 185 | } 186 | 187 | /** 188 | * Changes the current route of the page and then fires the callback when the page has loaded 189 | * 190 | * @param {String} path The given path that the current route will be changed to 191 | * @param {function} [callback] The given callback to fire once the view has been fully loaded 192 | * @method visit 193 | */ 194 | function visit(path, callback) { 195 | 196 | // wait until view shows up 197 | /* jshint -W106 */ 198 | $rootScope.__VIEW_STATUS = ++$viewCounter; 199 | /* jshint +W106 */ 200 | until(function() { 201 | return parseInt($terminalElement.attr('status')) >= $viewCounter; 202 | }, function() { 203 | // give it another tick to settle 204 | setTimeout(callback || noop, 0); 205 | }); 206 | 207 | // tell router to visit the view 208 | var fn = function() { 209 | $injector.get('$location').path(path); 210 | }; 211 | $rootScope.$$phase ? fn() : $rootScope.$apply(fn); 212 | } 213 | 214 | /** 215 | * Keeps checking an expression until it returns a truthy value and then runs the provided callback 216 | * 217 | * @param {function} exp The given function to poll 218 | * @param {function} callback The given callback to fire once the exp function returns a truthy value 219 | * @method until 220 | */ 221 | function until(exp, callback) { 222 | var timer, delay = 50; 223 | timer = setInterval(function() { 224 | if (exp()) { 225 | clearTimeout(timer); 226 | callback(); 227 | } 228 | }, delay); 229 | $timers.push(timer); 230 | } 231 | } 232 | } 233 | }; 234 | 235 | })(); 236 | -------------------------------------------------------------------------------- /dist/bard.js: -------------------------------------------------------------------------------- 1 | /** 2 | * bardjs - Spec helpers for testing angular v.1.x apps with Mocha, Jasmine or QUnit 3 | * @authors John Papa,Ward Bell 4 | * @version v0.1.10 5 | * @link https://github.com/wardbell/bardjs 6 | * @license MIT 7 | */ 8 | /*jshint -W079, -W117 */ 9 | (function() { 10 | 11 | var bard = { 12 | $httpBackend: $httpBackendReal, 13 | $q: $qReal, 14 | addGlobals: addGlobals, 15 | appModule: appModule, 16 | assertFail: assertFail, 17 | asyncModule: asyncModule, 18 | debugging: bardDebugging, 19 | fakeLogger: fakeLogger, 20 | fakeRouteHelperProvider: fakeRouteHelperProvider, 21 | fakeRouteProvider: fakeRouteProvider, 22 | fakeStateProvider: fakeStateProvider, 23 | fakeToastr: fakeToastr, 24 | inject: bardInject, 25 | log: bardLog, 26 | mochaRunnerListener: mochaRunnerListener, 27 | mockService: mockService, 28 | replaceAccentChars: replaceAccentChars, 29 | verifyNoOutstandingHttpRequests: verifyNoOutstandingHttpRequests, 30 | wrapWithDone: wrapWithDone 31 | }; 32 | 33 | var global = (function() { return this; })(); 34 | 35 | // mocha/jasmine/QUnit fns 36 | var afterEach = global.afterEach || global.teardown; 37 | var beforeEach = global.beforeEach || global.setup; 38 | 39 | var clearInject = []; 40 | var currentSpec = null; 41 | var debugging = false; 42 | var logCounter = 0; 43 | var okGlobals = []; 44 | 45 | addBindPolyfill(); 46 | 47 | beforeEach(function bardTopBeforeEach() { 48 | currentSpec = this; 49 | }); 50 | 51 | afterEach(function bardTopAfterEach() { 52 | currentSpec = null; 53 | bard.log('clearing injected globals: ' + clearInject); 54 | angular.forEach(clearInject, function(name) { 55 | delete global[name]; 56 | }); 57 | clearInject.length = 0; 58 | okGlobals.length = 0; 59 | }); 60 | 61 | global.bard = angular.extend(global.bard || {}, bard); 62 | 63 | //////////////////////// 64 | 65 | /*jshint -W101 */ 66 | /** 67 | * Replaces the ngMock'ed $httpBackend with the real one from ng thus 68 | * restoring the ability to issue AJAX calls to the backend with $http. 69 | * 70 | * Note that $q remains ngMocked so you must flush $http calls ($rootScope.$digest). 71 | * Use $rootScope.$apply() for this purpose. 72 | * 73 | * Could restore $q with $qReal in which case don't need to flush. 74 | * 75 | * Inspired by this StackOverflow answer: 76 | * http://stackoverflow.com/questions/20864764/e2e-mock-httpbackend-doesnt-actually-passthrough-for-me/26992327?iemail=1&noredirect=1#26992327 77 | * 78 | * Usage: 79 | * 80 | * var myService; 81 | * 82 | * beforeEach(module(bard.$httpBackend, 'app'); 83 | * 84 | * beforeEach(inject(function(_myService_) { 85 | * myService = _myService_; 86 | * })); 87 | * 88 | * it('should return valid data', function(done) { 89 | * myService.remoteCall() 90 | * .then(function(data) { 91 | * expect(data).toBeDefined(); 92 | * }) 93 | * .then(done, done); 94 | * 95 | * // because not using $qReal, must flush the $http and $q queues 96 | * $rootScope.$apply; 97 | * }); 98 | */ 99 | /*jshint +W101 */ 100 | function $httpBackendReal($provide) { 101 | $provide.provider('$httpBackend', function() { 102 | /*jshint validthis:true */ 103 | this.$get = function() { 104 | return angular.injector(['ng']).get('$httpBackend'); 105 | }; 106 | }); 107 | } 108 | 109 | /** 110 | * Replaces the ngMock'ed $q with the real one from ng thus 111 | * obviating the need to flush $http and $q queues 112 | * at the expense of ability to control $q timing. 113 | * 114 | * Usage: 115 | * 116 | * var myService; 117 | * 118 | * // Consider: beforeEach(bard.asyncModule('app')); 119 | * 120 | * beforeEach(module(bard.$q, bard.$httpBackend, 'app'); 121 | * 122 | * beforeEach(inject(function(_myService_) { 123 | * myService = _myService_; 124 | * })); 125 | * 126 | * it('should return valid data', function(done) { 127 | * myService.remoteCall() 128 | * .then(function(data) { 129 | * expect(data).toBeDefined(); 130 | * }) 131 | * .then(done, done); 132 | * 133 | * // not need to flush 134 | * }); 135 | */ 136 | function $qReal($provide) { 137 | $provide.provider('$q', function() { 138 | /*jshint validthis:true */ 139 | this.$get = function() { 140 | return angular.injector(['ng']).get('$q'); 141 | }; 142 | }); 143 | } 144 | /** 145 | * Add names of globals to list of OK globals for this mocha spec 146 | * NB: Call this method ONLY if you're using mocha! 147 | * NB: Turn off browser-sync else mocha detects the browser-sync globals 148 | * like ` ___browserSync___` 149 | * 150 | * usage: 151 | * addGlobals(this, 'foo'); // where `this` is the spec context 152 | * addGlobals(this, 'foo', bar); 153 | * addGlobals.bind(this)('foo', 'bar'); 154 | * addGlobals(ctx, ['foo', 'bar']) // where ctx is the spec context 155 | */ 156 | function addGlobals() { 157 | var args = Array.prototype.slice.call(arguments); 158 | var ctx = getCtxFromArgs.bind(this)(args); 159 | var globs = angular.isArray(args[0]) ? args[0] : args; 160 | angular.forEach(globs, function(g) { 161 | if (okGlobals.indexOf(g) === -1) { 162 | okGlobals.push(g); 163 | } 164 | }); 165 | // if a mocha test, add the ok globals to it 166 | ctx && ctx.test && ctx.test.globals && ctx.test.globals(okGlobals); 167 | } 168 | 169 | /** 170 | * Prepare ngMocked application feature module 171 | * along with faked toastr, routehelper, 172 | * and faked router services. 173 | * Especially useful for controller testing 174 | * Use it as you would the ngMocks#module method 175 | * 176 | * DO NOT USE IF YOU NEED THE REAL ROUTER SERVICES! 177 | * Fall back to `angular.mock.module(...)` or just `module(...)` 178 | * 179 | * Useage: 180 | * beforeEach(bard.appModule('app.avengers')); 181 | * 182 | * Equivalent to: 183 | * beforeEach(angular.mock.module( 184 | * 'app.avengers', 185 | * bard.fakeToastr, 186 | * bard.fakeRouteHelperProvider, 187 | * bard.fakeRouteProvider, 188 | * bard.fakeStateProvider) 189 | * ); 190 | */ 191 | function appModule() { 192 | var args = Array.prototype.slice.call(arguments, 0); 193 | args = args.concat(fakeRouteHelperProvider, fakeRouteProvider, 194 | fakeStateProvider, fakeToastr); 195 | return angular.mock.module.apply(angular.mock, args); 196 | } 197 | 198 | /** 199 | * Assert a failure in mocha, without condition 200 | * 201 | * Useage: 202 | * assertFail('you are hosed') 203 | * 204 | * Responds: 205 | * AssertionError: you are hosed 206 | * at Object.assertFail (..../test/lib/bard.js:153:15) 207 | * at Context. (.../....spec.js:329:15) 208 | * 209 | * OR JUST THROW the chai.AssertionError and treat this 210 | * as a reminder of how to do it. 211 | */ 212 | function assertFail(message) { 213 | throw new chai.AssertionError(message); 214 | } 215 | 216 | /** 217 | * Prepare ngMocked module definition that makes real $http and $q calls 218 | * Also adds fakeLogger to the end of the definition 219 | * Use it as you would the ngMocks#module method 220 | * 221 | * Useage: 222 | * beforeEach(bard.asyncModule('app')); 223 | * 224 | * Equivalent to: 225 | * beforeEach(module('app', bard.$httpBackend, bard.$q, bard.fakeToastr)); 226 | */ 227 | function asyncModule() { 228 | var args = Array.prototype.slice.call(arguments, 0); 229 | args = args.concat($httpBackendReal, $qReal, fakeToastr); 230 | // build and return the ngMocked test module 231 | return angular.mock.module.apply(angular.mock, args); 232 | } 233 | 234 | /** 235 | * get/set bard debugging flag 236 | */ 237 | function bardDebugging(x) { 238 | if (typeof x !== 'undefined') { debugging = !!x; } 239 | return debugging; 240 | } 241 | 242 | /** 243 | * Write to console if bard debugging flag is on 244 | */ 245 | function bardLog(msg) { 246 | if (debugging) { 247 | console.log('---bard (' + (logCounter += 1) + ') ' + msg); 248 | } 249 | } 250 | 251 | /** 252 | * inject selected services into the windows object during test 253 | * then remove them when test ends with an `afterEach`. 254 | * 255 | * spares us the repetition of creating common service vars and injecting them 256 | * 257 | * Option: the first argument may be the mocha spec context object (`this`) 258 | * It MUST be `this` if you what to check for mocha global leaks. 259 | * Do NOT supply `this` as the first arg if you're not running mocha specs. 260 | * 261 | * remaining inject arguments may take one of 3 forms : 262 | * 263 | * function - This fn will be passed to ngMocks.inject. 264 | * Annotations extracted after inject does its thing. 265 | * [strings] - same string array you'd use to set fn.$inject 266 | * (...string) - string arguments turned into a string array 267 | 268 | * 269 | * usage: 270 | * 271 | * bard.inject(this, ...); // `this` === the spec context 272 | * 273 | * bard.inject(this, '$log', 'dataservice'); 274 | * bard.inject(this, ['$log', 'dataservice']); 275 | * bard.inject(this, function($log, dataservice) { ... }); 276 | * 277 | */ 278 | function bardInject () { 279 | var args = Array.prototype.slice.call(arguments); 280 | var ctx = getCtxFromArgs.bind(this)(args); 281 | var first = args[0]; 282 | 283 | if (typeof first === 'function') { 284 | // use ngMocks.inject to execute the func in the arg 285 | angular.mock.inject(first); 286 | args = first.$inject; 287 | if (!args) { 288 | // unfortunately ngMocks.inject only prepares inject.$inject for us 289 | // if using strictDi as of v.1.3.8 290 | // therefore, apply its annotation extraction logic manually 291 | args = getinjectargs(first); 292 | } 293 | } 294 | else if (angular.isArray(first)) { 295 | args = first; // assume is an array of strings 296 | } 297 | // else assume all args are strings 298 | 299 | var $injector = currentSpec.$injector; 300 | if (!$injector) { 301 | angular.mock.inject(); // create the injector 302 | $injector = currentSpec.$injector; 303 | } 304 | 305 | var names = []; 306 | angular.forEach(args, function(name, ix) { 307 | 308 | if (typeof name !== 'string') { 309 | return; // WAT? Only strings allowed. Let's skip it and move on. 310 | } 311 | var value = $injector.get(name); 312 | if (value == null) { return; } 313 | 314 | var pathName = name.split('.'); 315 | 316 | if (pathName.length > 1) { 317 | // name is a path like 'block.foo'. Can't use as identifier 318 | // assume last segment should be identifier name, e.g. 'foo' 319 | name = pathName[pathName.length - 1]; 320 | // todo: tolerate component names that are invalid JS identifiers, e.g. 'burning man' 321 | } 322 | global[name] = value; 323 | clearInject.push(name); 324 | names.push(name); 325 | }); 326 | 327 | bard.addGlobals.bind(ctx)(names); 328 | } 329 | 330 | function fakeLogger($provide) { 331 | $provide.value('logger', sinon.stub({ 332 | info: function() {}, 333 | error: function() {}, 334 | warning: function() {}, 335 | success: function() {} 336 | })); 337 | } 338 | 339 | function fakeToastr($provide) { 340 | $provide.constant('toastr', sinon.stub({ 341 | info: function() {}, 342 | error: function() {}, 343 | warning: function() {}, 344 | success: function() {} 345 | })); 346 | } 347 | 348 | function fakeRouteHelperProvider($provide) { 349 | $provide.provider('routehelper', function() { 350 | /* jshint validthis:true */ 351 | this.config = { 352 | $routeProvider: undefined, 353 | docTitle: 'Testing' 354 | }; 355 | this.$get = function() { 356 | return { 357 | configureRoutes: sinon.stub(), 358 | getRoutes: sinon.stub().returns([]), 359 | routeCounts: { 360 | errors: 0, 361 | changes: 0 362 | } 363 | }; 364 | }; 365 | }); 366 | } 367 | 368 | function fakeRouteProvider($provide) { 369 | /** 370 | * Stub out the $routeProvider so we avoid 371 | * all routing calls, including the default route 372 | * which runs on every test otherwise. 373 | * Make sure this goes before the inject in the spec. 374 | * 375 | * Optionally set up the fake behavior in your tests by monkey patching 376 | * the faked $route router. For example: 377 | * 378 | * beforeEach(function() { 379 | * // get fake $route router service 380 | * bard.inject(this, '$route'); 381 | * 382 | * // plug in fake $route router values for this set of tests 383 | * $route.current = { ... fake values here ... }; 384 | * $route.routes = { ... fake values here ... }; 385 | * }) 386 | */ 387 | $provide.provider('$route', function() { 388 | /* jshint validthis:true */ 389 | this.when = sinon.stub(); 390 | this.otherwise = sinon.stub(); 391 | 392 | this.$get = function() { 393 | return { 394 | // current: {}, // fake before each test as needed 395 | // routes: {} // fake before each test as needed 396 | // more? You'll know when it fails :-) 397 | _faked: 'this is the faked $route service' 398 | }; 399 | }; 400 | }); 401 | } 402 | 403 | function fakeStateProvider($provide) { 404 | /** 405 | * Stub out the $stateProvider so we avoid 406 | * all routing calls, including the default state 407 | * which runs on every test otherwise. 408 | * Make sure this goes before the inject in the spec. 409 | * 410 | * Optionally set up the fake behavior in your tests by monkey patching 411 | * the faked $state router. For example: 412 | * 413 | * beforeEach(function() { 414 | * // get fake $state router service 415 | * bard.inject(this, '$state'); 416 | * 417 | * // plug in fake $state router values for this set of tests 418 | * $state.current = { ... fake values here ... }; 419 | * $state.state = { ... fake values here ... }; 420 | * }) 421 | */ 422 | $provide.provider('$state', function() { 423 | /* jshint validthis:true */ 424 | this.state = sinon.stub(); 425 | 426 | this.$get = function() { 427 | return { 428 | // current: {}, // fake before each test as needed 429 | // state: {} // fake before each test as needed 430 | // more? You'll know when it fails :-) 431 | _faked: 'this is the faked $state service' 432 | }; 433 | }; 434 | }); 435 | $provide.provider('$urlRouter', function() { 436 | /* jshint validthis:true */ 437 | this.otherwise = sinon.stub(); 438 | 439 | this.$get = function() { 440 | return { 441 | // current: {}, // fake before each test as needed 442 | // states: {} // fake before each test as needed 443 | // more? You'll know when it fails :-) 444 | _faked: 'this is the faked $urlRouter service' 445 | }; 446 | }; 447 | }); 448 | } 449 | 450 | /** 451 | * Get the spec context from parameters (if there) 452 | * or from `this` (if it is the ctx as a result of `bind`) 453 | */ 454 | function getCtxFromArgs(args) { 455 | var ctx; 456 | var first = args[0]; 457 | // heuristic to discover if the first arg is the mocha spec context (`this`) 458 | if (first && first.test) { 459 | // The first arg was the mocha spec context (`this`) 460 | // Get it and strip it from args 461 | ctx = args.shift(); 462 | } else if (this.test) { 463 | // alternative: caller can bind bardInject to the spec context 464 | ctx = this; 465 | } 466 | return ctx; 467 | } 468 | 469 | /** 470 | * Inspired by Angular; that's how they get the parms for injection 471 | * Todo: no longer used by `injector`. Remove? 472 | */ 473 | function getFnParams(fn) { 474 | var fnText; 475 | var argDecl; 476 | 477 | var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; 478 | var FN_ARG_SPLIT = /,/; 479 | var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; 480 | var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; 481 | var params = []; 482 | if (fn.length) { 483 | fnText = fn.toString().replace(STRIP_COMMENTS, ''); 484 | argDecl = fnText.match(FN_ARGS); 485 | angular.forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) { 486 | arg.replace(FN_ARG, function(all, underscore, name) { 487 | params.push(name); 488 | }); 489 | }); 490 | } 491 | return params; 492 | } 493 | 494 | function isSpecRunning() { return !!currentSpec; } 495 | 496 | /** 497 | * Mocks out a service with sinon stubbed functions 498 | * that return the values specified in the config 499 | * 500 | * If the config value is `undefined`, 501 | * stub the service method with a dummy that doesn't return a value 502 | * 503 | * If the config value is a function, set service property with it 504 | * 505 | * If a service member is a property, not a function, 506 | * set it with the config value 507 | 508 | * If a service member name is not a key in config, 509 | * follow the same logic as above to set its members 510 | * using the config._default value (which is `undefined` if omitted) 511 | * 512 | * If there is a config entry that is NOT a member of the service 513 | * add mocked function to the service using the config value 514 | * 515 | * Usage: 516 | * Given this DoWork service: 517 | * { 518 | * doWork1: an async function, 519 | * doWork2: a function, 520 | * doWork3: an async function, 521 | * doWork4: a function, 522 | * isActive: true 523 | * } 524 | * 525 | * Given this config: 526 | * { 527 | * doWork1: $q.when([{name: 'Bob'}, {name: 'Sally'}]), 528 | * doWork2: undefined, 529 | * //doWork3: not in config therefore will get _default value 530 | * doWork4: an alternate doWork4 function 531 | * doWork5: $q.reject('bad boy!') 532 | * isActive: false, 533 | * _default: $q.when([]) 534 | * } 535 | * 536 | * Service becomes 537 | * { 538 | * doWork1: a stub returning $q.when([{name: 'Bob'}, {name: 'Sally'}]), 539 | * doWork2: do-nothing stub, 540 | * doWork3: a stub returning $q.when([]), 541 | * doWork4: an alternate doWork4 function, 542 | * doWork5: a stub returning $q.reject('bad boy!'), 543 | * isActive: false, 544 | * } 545 | */ 546 | function mockService(service, config) { 547 | 548 | var serviceKeys = []; 549 | for (var key in service) { 550 | serviceKeys.push(key); 551 | } 552 | 553 | var configKeys = []; 554 | for (var key in config) { 555 | configKeys.push(key); 556 | } 557 | 558 | angular.forEach(serviceKeys, function(key) { 559 | var value = configKeys.indexOf(key) > -1 ? 560 | config[key] : config._default; 561 | 562 | if (typeof service[key] === 'function') { 563 | if (typeof value === 'function') { 564 | service[key] = value; 565 | } else { 566 | sinon.stub(service, key, function() { 567 | return value; 568 | }); 569 | } 570 | } else { 571 | service[key] = value; 572 | } 573 | }); 574 | 575 | // for all unused config entries add a sinon stubbed 576 | // async method that returns the config value 577 | angular.forEach(configKeys, function(key) { 578 | if (serviceKeys.indexOf(key) === -1) { 579 | var value = config[key]; 580 | if (typeof value === 'function') { 581 | service[key] = value; 582 | } else { 583 | service[key] = sinon.spy(function() { 584 | return value; 585 | }); 586 | } 587 | } 588 | }); 589 | 590 | return service; 591 | } 592 | 593 | /** 594 | * Listen to mocha test runner events 595 | * Usage in browser: 596 | * var runner = mocha.run(); 597 | * bard.mochaRunnerListener(runner); 598 | */ 599 | function mochaRunnerListener(runner) { 600 | if (!global.mocha) { return; } 601 | if (!runner.ignoreLeaks) { 602 | runner.on('hook end', addOkGlobals); 603 | }; 604 | 605 | // When checking global leaks with mocha.checkLeaks() 606 | // make sure mocha is aware of bard's okGlobals 607 | function addOkGlobals(hook) { 608 | // HACK: only way I've found so far to ensure that bard added globals 609 | // are always inspected. Using private mocha _allowedGlobals (shhhh!) 610 | if (okGlobals.length && !hook._allowedGlobals) { 611 | hook._allowedGlobals = okGlobals; 612 | } 613 | } 614 | } 615 | 616 | // Replaces the accented characters of many European languages w/ unaccented chars 617 | // Use it in JavaScript string sorts where such characters may be encountered 618 | // Matches the default string comparers of most databases. 619 | // Ex: replaceAccentChars(a.Name) < replaceAccentChars(b.Name) 620 | // instead of: a.Name < b.Name 621 | function replaceAccentChars(s) { 622 | var r = s.toLowerCase(); 623 | r = r.replace(new RegExp(/[àáâãäå]/g), 'a'); 624 | r = r.replace(new RegExp(/æ/g), 'ae'); 625 | r = r.replace(new RegExp(/ç/g), 'c'); 626 | r = r.replace(new RegExp(/[èéêë]/g), 'e'); 627 | r = r.replace(new RegExp(/[ìíîï]/g), 'i'); 628 | r = r.replace(new RegExp(/ñ/g), 'n'); 629 | r = r.replace(new RegExp(/[òóôõö]/g), 'o'); 630 | r = r.replace(new RegExp(/œ/g), 'oe'); 631 | r = r.replace(new RegExp(/[ùúûü]/g), 'u'); 632 | r = r.replace(new RegExp(/[ýÿ]/g), 'y'); 633 | return r; 634 | } 635 | 636 | /** 637 | * Assert that there are no outstanding HTTP requests after test is complete 638 | * For use with ngMocks; doesn't work for async server integration tests 639 | */ 640 | function verifyNoOutstandingHttpRequests () { 641 | afterEach(angular.mock.inject(function($httpBackend) { 642 | $httpBackend.verifyNoOutstandingExpectation(); 643 | $httpBackend.verifyNoOutstandingRequest(); 644 | })); 645 | } 646 | 647 | /** 648 | * Returns a function that execute a callback function 649 | * (typically a fn making asserts) within a try/catch 650 | * The try/catch then calls the ambient "done" function 651 | * in the appropriate way for both success and failure 652 | * 653 | * Useage: 654 | * bard.inject('ngRouteTester', ...); // see bard-ngRouteTester.js 655 | * ... 656 | * // When the DOM is ready, assert got the dashboard view 657 | * ngRouteTester.until(elemIsReady, wrap(hasDashboardView, done)); 658 | */ 659 | function wrapWithDone(callback, done) { 660 | return function() { 661 | try { 662 | callback(); 663 | done(); 664 | } catch (err) { 665 | done(err); 666 | } 667 | }; 668 | } 669 | 670 | /* 671 | * Phantom.js does not support Function.prototype.bind (at least not before v.2.0 672 | * That's just crazy. Everybody supports bind. 673 | * Read about it here: https://groups.google.com/forum/#!msg/phantomjs/r0hPOmnCUpc/uxusqsl2LNoJ 674 | * This polyfill is copied directly from MDN 675 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#Compatibility 676 | */ 677 | function addBindPolyfill() { 678 | if (Function.prototype.bind) { return; } // already defined 679 | 680 | /*jshint freeze: false */ 681 | Function.prototype.bind = function (oThis) { 682 | if (typeof this !== 'function') { 683 | // closest thing possible to the ECMAScript 5 684 | // internal IsCallable function 685 | throw new TypeError( 686 | 'Function.prototype.bind - what is trying to be bound is not callable'); 687 | } 688 | 689 | var aArgs = Array.prototype.slice.call(arguments, 1), 690 | fToBind = this, 691 | FuncNoOp = function () {}, 692 | fBound = function () { 693 | return fToBind.apply(this instanceof FuncNoOp && oThis ? this : oThis, 694 | aArgs.concat(Array.prototype.slice.call(arguments))); 695 | }; 696 | 697 | FuncNoOp.prototype = this.prototype; 698 | fBound.prototype = new FuncNoOp(); 699 | 700 | return fBound; 701 | }; 702 | } 703 | 704 | })(); 705 | -------------------------------------------------------------------------------- /dist/bard.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * bardjs - Spec helpers for testing angular v.1.x apps with Mocha, Jasmine or QUnit 3 | * @authors John Papa,Ward Bell 4 | * @version v0.1.10 5 | * @link https://github.com/wardbell/bardjs 6 | * @license MIT 7 | */ 8 | !function(){window.bard=window.bard||{},window.bard.ngRouteTester=function(e){function t(t){function n(){p=u.document,i=angular.element(p.createElement("div")),t.value("$rootElement",i);var e=u.mockLocationPaths;if((null==e?!0:e)&&t.decorator("$location",a),u.templateUrl&&o(),u.template){i.html(u.template);var n=angular.element(i[0].querySelector(f));c=n.parent()}else c=angular.element("
"),i.append(c)}function a(e,t){var n=e.path();return e.path=function(e){return e?(n!==e&&(n=e,t.$broadcast("$locationChangeSuccess",e)),this):n},e}function o(){var e=new XMLHttpRequest;if(e.open("GET",u.templateUrl,!1),e.send(null),200!==e.status)throw new Error("ngRouteTester: Unable to download template file");u.template=e.responseText}function r(e,t,n,a){function o(){l=angular.element('
'),i.append(l),n.$apply(function(){i.data("$injector",t),e(i)(n),angular.element(p.body).append(i)})}function r(){angular.forEach(d,function(e){clearTimeout(e)});var e=angular.element(document.body);e.removeData(),i.remove(),n.$destroy()}function u(){return t.get("$location").path()}function s(){return angular.element(c[0].querySelector(f))}function $(e,a){n.__VIEW_STATUS=++m,g(function(){return parseInt(l.attr("status"))>=m},function(){setTimeout(a||v,0)});var o=function(){t.get("$location").path(e)};n.$$phase?o():n.$apply(o)}function g(e,t){var n,a=50;n=setInterval(function(){e()&&(clearTimeout(n),t())},a),d.push(n)}return o(),afterEach&&afterEach(r),{$injector:t,$rootScope:n,$route:a,path:u,rootElement:i,until:g,viewElement:s,visit:$}}var u={document:document};angular.extend(u,e),n(),t.factory("ngRouteTester",r);var i,c,l,p,d=[],m=0,v=angular.noop,f="ng-view, [ng-view], .ng-view, [x-ng-view], [data-ng-view]";a.$inject=["$delegate","$rootScope"],r.$inject=["$compile","$injector","$rootScope","$route"]}return t.$inject=["$provide"],t}}(); 9 | -------------------------------------------------------------------------------- /gulp.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | var root = './'; 3 | var service = { 4 | getConfig: getConfig 5 | }; 6 | return service; 7 | 8 | function getConfig() { 9 | var config = { 10 | specs: ['./tests/**/*.spec.js'], 11 | js: [ 12 | './bard.js', 13 | './bard-ngRouteTester.js' 14 | ], 15 | packages: [ 16 | './package.json', 17 | './bower.json' 18 | ], 19 | build: './dist/', 20 | report: './report/', 21 | root: root 22 | }; 23 | 24 | return config; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var args = require('yargs').argv; 3 | var config = require('./gulp.config')().getConfig(); 4 | var del = require('del'); 5 | var glob = require('glob'); 6 | var _ = require('lodash'); 7 | var path = require('path'); 8 | var $ = require('gulp-load-plugins')({lazy: true}); 9 | 10 | var colors = $.util.colors; 11 | var env = $.util.env; 12 | 13 | /** 14 | * List the available gulp tasks 15 | */ 16 | gulp.task('help', $.taskListing); 17 | gulp.task('default', ['help']); 18 | 19 | /** 20 | * Lint the code, create coverage report, and a visualizer 21 | * @return {Stream} 22 | */ 23 | gulp.task('vet', function() { 24 | log('Analyzing source with JSHint and JSCS'); 25 | 26 | return gulp 27 | .src(config.js) 28 | .pipe($.if(env.verbose, $.print())) 29 | .pipe($.jshint.reporter('jshint-stylish')) 30 | .pipe($.jscs()); 31 | }); 32 | 33 | /** 34 | * Bump the version 35 | * --type=pre will bump the prerelease version *.*.*-x 36 | * --type=patch or no flag will bump the patch version *.*.x 37 | * --type=minor will bump the minor version *.x.* 38 | * --type=major will bump the major version x.*.* 39 | * --version=1.2.3 will bump to a specific version and ignore other flags 40 | */ 41 | gulp.task('bump', function() { 42 | var msg = 'Bumping versions'; 43 | var type = args.type; 44 | var version = args.ver; 45 | var options = {}; 46 | if (version) { 47 | options.version = version; 48 | msg += ' to ' + version; 49 | } else { 50 | options.type = type; 51 | msg += ' for a ' + type; 52 | } 53 | log(msg); 54 | 55 | return gulp 56 | .src(config.packages) 57 | .pipe($.print()) 58 | .pipe($.bump(options)) 59 | .pipe(gulp.dest(config.root)); 60 | }); 61 | 62 | /** 63 | * Build everything 64 | */ 65 | gulp.task('build', ['clean'], function(done) { 66 | log('Optimizing the js, css, and html'); 67 | 68 | var stream = gulp 69 | .src(config.js) 70 | .pipe(getHeader()) 71 | .pipe(gulp.dest(config.build)) 72 | .pipe($.uglify()) 73 | .pipe(getHeader()) 74 | .pipe($.rename('bard.min.js')) 75 | .pipe(gulp.dest(config.build)) 76 | .on('end', success) 77 | .on('error', error); 78 | 79 | function error(err) { 80 | log(err); 81 | done(err); 82 | } 83 | 84 | function success() { 85 | var msg = { 86 | title: 'gulp build', 87 | message: 'Deployed to the dist folder' 88 | }; 89 | log(msg); 90 | notify(msg); 91 | done(); 92 | } 93 | }); 94 | 95 | /** 96 | * Remove all files from the build, temp, and reports folders 97 | * @param {Function} done - callback when complete 98 | */ 99 | gulp.task('clean', function(done) { 100 | var delconfig = [].concat(config.build, config.report); 101 | log('Cleaning: ' + $.util.colors.blue(delconfig)); 102 | del(delconfig, done); 103 | }); 104 | 105 | /** 106 | * Delete all files in a given path 107 | * @param {Array} path - array of paths to delete 108 | * @param {Function} done - callback when complete 109 | */ 110 | function clean(path, done) { 111 | log('Cleaning: ' + $.util.colors.blue(path)); 112 | del(path, done); 113 | } 114 | 115 | /** 116 | * Formatter for bytediff to display the size changes after processing 117 | * @param {Object} data - byte data 118 | * @return {String} Difference in bytes, formatted 119 | */ 120 | function bytediffFormatter(data) { 121 | var difference = (data.savings > 0) ? ' smaller.' : ' larger.'; 122 | return data.fileName + ' went from ' + 123 | (data.startSize / 1000).toFixed(2) + ' kB to ' + 124 | (data.endSize / 1000).toFixed(2) + ' kB and is ' + 125 | formatPercent(1 - data.percent, 2) + '%' + difference; 126 | } 127 | 128 | /** 129 | * Format a number as a percentage 130 | * @param {Number} num Number to format as a percent 131 | * @param {Number} precision Precision of the decimal 132 | * @return {String} Formatted perentage 133 | */ 134 | function formatPercent(num, precision) { 135 | return (num * 100).toFixed(precision); 136 | } 137 | 138 | /** 139 | * Format and return the header for files 140 | * @return {String} Formatted file header 141 | */ 142 | function getHeader() { 143 | var pkg = require('./package.json'); 144 | var template = ['/**', 145 | ' * <%= pkg.name %> - <%= pkg.description %>', 146 | ' * @authors <%= pkg.authors %>', 147 | ' * @version v<%= pkg.version %>', 148 | ' * @link <%= pkg.homepage %>', 149 | ' * @license <%= pkg.license %>', 150 | ' */', 151 | '' 152 | ].join('\n'); 153 | return $.header(template, { 154 | pkg: pkg 155 | }); 156 | } 157 | 158 | /** 159 | * Log a message or series of messages using chalk's blue color. 160 | * Can pass in a string, object or array. 161 | */ 162 | function log(msg) { 163 | if (typeof(msg) === 'object') { 164 | for (var item in msg) { 165 | if (msg.hasOwnProperty(item)) { 166 | $.util.log($.util.colors.blue(msg[item])); 167 | } 168 | } 169 | } else { 170 | $.util.log($.util.colors.blue(msg)); 171 | } 172 | } 173 | 174 | /** 175 | * Show OS level notification using node-notifier 176 | */ 177 | function notify(options) { 178 | var notifier = require('node-notifier'); 179 | var notifyOptions = { 180 | sound: 'Bottle', 181 | contentImage: path.join(__dirname, 'gulp.png'), 182 | icon: path.join(__dirname, 'gulp.png') 183 | }; 184 | _.assign(notifyOptions, options); 185 | notifier.notify(notifyOptions); 186 | } 187 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | bardjs tests 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |

bardjs tests

26 | 27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'firebait:bardjs', 3 | version: '0.0.4', 4 | // Brief, one-line summary of the package. 5 | summary: 'bardjs is a small library of functions to help you write Angular v.1.x application tests.', 6 | // URL to the Git repository containing the source code for this package. 7 | git: 'https://github.com/firebait/bardjs.git', 8 | // By default, Meteor will default to using README.md for documentation. 9 | // To avoid submitting documentation, set this field to null. 10 | documentation: 'README.md', 11 | debugOnly: true 12 | }); 13 | 14 | Package.onUse(function(api) { 15 | api.versionsFrom('1.2.0.2'); 16 | api.use([ 17 | 'angular:angular@1.4.4', 18 | 'angular:angular-mocks@1.4.7', 19 | 'practicalmeteor:sinon@1.14.1_2' 20 | ], 'client'); 21 | api.addFiles([ 22 | './dist/bard.js', 23 | './dist/bard-ngRouteTester.js'], 'client'); 24 | }); 25 | 26 | Package.onTest(function(api) { 27 | }); 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bardjs", 3 | "version": "0.1.10", 4 | "description": "Spec helpers for testing angular v.1.x apps with Mocha, Jasmine or QUnit", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/wardbell/bardjs.git" 8 | }, 9 | "keywords": [ 10 | "angular", 11 | "mocha", 12 | "chai", 13 | "mocks", 14 | "testing", 15 | "client-side" 16 | ], 17 | "authors": [ 18 | "John Papa", 19 | "Ward Bell" 20 | ], 21 | "license": "MIT", 22 | "homepage": "https://github.com/wardbell/bardjs", 23 | "bugs": { 24 | "url": "https://github.com/wardbell/bardjs/issues" 25 | }, 26 | "scripts": { 27 | "init": "npm install" 28 | }, 29 | "dependencies": { 30 | "sinon": "~1.15.0" 31 | }, 32 | "devDependencies": { 33 | "chai": "^1.9.1", 34 | "del": "^1.1.0", 35 | "glob": "^4.3.2", 36 | "gulp": "^3.8.10", 37 | "gulp-bump": "^0.2.2", 38 | "gulp-bytediff": "^0.2.0", 39 | "gulp-header": "^1.2.2", 40 | "gulp-if": "^1.2.5", 41 | "gulp-jscs": "^1.3.1", 42 | "gulp-jshint": "^1.9.0", 43 | "gulp-load-plugins": "^0.8.0", 44 | "gulp-minify-html": "^0.1.7", 45 | "gulp-print": "^1.1.0", 46 | "gulp-rename": "^1.2.0", 47 | "gulp-task-listing": "^1.0.0", 48 | "gulp-uglify": "^1.0.2", 49 | "gulp-util": "^3.0.1", 50 | "jshint-stylish": "^1.0.0", 51 | "karma": "^0.12.24", 52 | "karma-chai": "^0.1.0", 53 | "karma-chai-sinon": "^0.1.3", 54 | "karma-chrome-launcher": "^0.1.4", 55 | "karma-coverage": "^0.2.4", 56 | "karma-firefox-launcher": "^0.1.3", 57 | "karma-growl-reporter": "^0.1.1", 58 | "karma-mocha": "^0.1.4", 59 | "karma-phantomjs-launcher": "^0.1.4", 60 | "karma-safari-launcher": "^0.1.1", 61 | "karma-sinon": "^1.0.3", 62 | "lodash": "^2.4.1", 63 | "mocha-clean": "^0.4.0", 64 | "node-notifier": "^4.0.3", 65 | "phantomjs": "1.9.9", 66 | "sinon-chai": "^2.6.0", 67 | "yargs": "^3.5.4" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /snippets/brackets-testing-snippets.yaml: -------------------------------------------------------------------------------- 1 | # mocha/jasmine snippets 2 | 3 | - trigger: bdescribe 4 | description: "mocha/jasmine describe" 5 | scope: javascript 6 | text: | 7 | describe('${1:description}', function () { 8 | ${2:} 9 | }); 10 | 11 | - trigger: bit 12 | description: "mocha/jasmine test (synchronous)" 13 | scope: javascript 14 | text: | 15 | it('${1:should}', function () { 16 | ${2:} 17 | }); 18 | 19 | - trigger: bait 20 | description: "asynchornous mocha/jasmine test" 21 | scope: javascript 22 | text: | 23 | it('${1:should}', function (done) { 24 | ${2:} 25 | }); 26 | 27 | - trigger: bbeforeEach 28 | description: "mocha/jasmine before-each-test" 29 | scope: javascript 30 | text: | 31 | beforeEach(function () { 32 | ${1:} 33 | }); 34 | 35 | - trigger: bafterEach 36 | description: "mocha/jasmine after-each-test" 37 | scope: javascript 38 | text: | 39 | afterEach(function () { 40 | ${1:} 41 | }); 42 | 43 | - trigger: bdone 44 | description: ".then(done, done) - tail of async promise chain" 45 | scope: javascript 46 | text: | 47 | .then(done, done); 48 | ${1:} 49 | 50 | # chai expectation snippets 51 | # see http://chaijs.com/api/bdd/ 52 | 53 | - trigger: bexpect 54 | description: "chai expect" 55 | scope: javascript 56 | text: | 57 | expect(${1:expected}).to.${2:}; 58 | 59 | - trigger: bequal 60 | description: "chai expect to equal" 61 | scope: javascript 62 | text: | 63 | expect(${1:expected}).to.equal(${2:value}); 64 | 65 | - trigger: blen 66 | description: "chai expect to have length" 67 | scope: javascript 68 | text: | 69 | expect(${1:expected}).to.have.length(${2:length}); 70 | 71 | - trigger: bmatch 72 | description: "chai expect to match" 73 | scope: javascript 74 | text: | 75 | expect(${1:expected}).to.match(/${2:match}/i); 76 | 77 | 78 | - trigger: bprop 79 | description: "chai expect to have property" 80 | scope: javascript 81 | text: | 82 | expect(${1:expected}).to.have.property('${2:property}', ${3:value}); 83 | 84 | - trigger: bcalled 85 | description: "chai expect to have been called" 86 | scope: javascript 87 | text: | 88 | expect(${1:expected}).to.have.been.called${2:}; 89 | 90 | - trigger: bthrow 91 | description: "chai expect to throw" 92 | scope: javascript 93 | text: | 94 | expect(function () { 95 | ${1:fn-that-throws}; 96 | }).to.throw(${2:Error}); 97 | 98 | # bard.js snippets 99 | 100 | - trigger: binject 101 | description: "bard inject" 102 | scope: javascript 103 | text: | 104 | bard.inject(this, ${1:quoted-dependencies}); 105 | 106 | - trigger: bcinject 107 | description: "bard inject controller" 108 | scope: javascript 109 | text: | 110 | bard.inject(this, '$controller', '$q', '$rootScope', ${1:}); 111 | 112 | - trigger: bmodule 113 | description: "bard app module" 114 | scope: javascript 115 | text: | 116 | bard.appModule('${1:app}'${2:}); 117 | 118 | - trigger: basyncmodule 119 | description: "bard async module" 120 | scope: javascript 121 | text: | 122 | bard.asyncModule('${1:app}'${2:}); 123 | 124 | - trigger: bverify 125 | description: "bard.verifyNoOutstandingHttpRequests();" 126 | scope: javascript 127 | text: | 128 | bard.verifyNoOutstandingHttpRequests(); 129 | ${1:} 130 | 131 | # angular test snippets 132 | 133 | - trigger: bapply 134 | description: "$rootScope.$apply();" 135 | scope: javascript 136 | text: | 137 | $rootScope.$apply(); 138 | ${1:} 139 | 140 | - trigger: bwhen 141 | description: "$httpBackend.when(...).respond(...);" 142 | scope: javascript 143 | text: | 144 | $httpBackend.when('get', '${1:url}') 145 | .respond(${2:status}, {${3:data}}); 146 | 147 | - trigger: bflush 148 | description: "$httpBackend.flush();" 149 | scope: javascript 150 | text: | 151 | $httpBackend.flush(); 152 | ${1:} 153 | 154 | # miscellaneous snippets 155 | - trigger: bfn 156 | description: "function template" 157 | scope: javascript 158 | text: | 159 | function ${1:}() { 160 | ${2:} 161 | } 162 | -------------------------------------------------------------------------------- /tests/bard.injector.spec.js: -------------------------------------------------------------------------------- 1 | /* jshint -W117, -W030 */ 2 | describe('bard.inject', function() { 3 | 'use strict'; 4 | 5 | var origDebugging; 6 | 7 | before(function() { 8 | origDebugging = bard.debugging(); 9 | // uncomment to turn bard debug logging on for this spec file 10 | // bard.debugging(true); 11 | }); 12 | 13 | after(function() { 14 | bard.debugging(origDebugging); // restore bard debug logging 15 | }); 16 | 17 | beforeEach(module(function($provide) { 18 | // define a 'nutz' service for testing injector 19 | $provide.service('nutz', function() {}); 20 | })); 21 | 22 | beforeEach('bard.inject.spec top beforeEach', function() { 23 | // Confirm no window pollution from a prior bard.inject() call 24 | bard.log('bard.inject.spec top beforeEach'); 25 | expect(window.$log).to.not.exist; 26 | expect(window.nutz).to.not.exist; 27 | expect(window.baz).to.not.exist; 28 | expect(window.foo).to.not.exist; 29 | }); 30 | 31 | describe('(describe #1):', function() { 32 | it('window.$log and window.nutz should not exist', function() { 33 | expect(window.$log).to.not.exist; 34 | expect(window.nutz).to.not.exist; 35 | }); 36 | }); 37 | 38 | describe('(describe #2):', function() { 39 | 40 | beforeEach('bard.inject.spec describe #2 beforeEach', function() { 41 | bard.log('bard.inject.spec (describe #2) beforeEach'); 42 | bard.inject(this, ['$log', 'nutz']); 43 | }); 44 | 45 | it('true is true', function() { 46 | expect(true).to.be.true; 47 | }); 48 | 49 | it('$log exists', function() { 50 | expect($log).to.exist; 51 | }); 52 | 53 | it('nutz exists', function() { 54 | expect(nutz).to.exist; 55 | }); 56 | }); 57 | 58 | describe('(describe #3):', function() { 59 | 60 | beforeEach('bard.inject.spec describe #3 beforeEach', function() { 61 | bard.log('bard.inject.spec (describe #3) beforeEach'); 62 | // window.$log and window.nutz should not exist before any test 63 | expect(window.$log).to.not.exist; 64 | expect(window.nutz).to.not.exist; 65 | }); 66 | 67 | // Although inject() puts injectables in the window, 68 | // it also removes them after each test 69 | // Notice ... no private vars for $log or nutz! 70 | // ... no injecting of them either. 71 | 72 | it('should set window.$log and window.nutz when call inject w/ string params', function() { 73 | 74 | bard.inject(this, '$log', 'nutz'); 75 | 76 | expect($log).to.exist; 77 | expect(nutz).to.exist; 78 | 79 | // They are actually in the window 80 | expect(window.$log).to.exist; 81 | expect(window.nutz).to.exist; 82 | 83 | }); 84 | 85 | it('should set window.$log and window.nutz when call inject with string array', function() { 86 | 87 | bard.inject(this, ['$log', 'nutz']); 88 | 89 | expect($log).to.exist; 90 | expect(nutz).to.exist; 91 | }); 92 | 93 | it('should set window.$log and window.nutz when call inject with a function', function() { 94 | 95 | bard.inject(this, function($log, nutz) { 96 | // do stuff just as if we called ngMocks.inject 97 | $log.info('use the injected $log'); 98 | }); 99 | 100 | expect($log).to.exist; 101 | expect(nutz).to.exist; 102 | 103 | expect($log.info.logs[0][0]) 104 | .to.equal('use the injected $log', 105 | '$log.info should have been called: '); 106 | }); 107 | 108 | // reinforcing the point that inject adds to globals, not local fn scope 109 | it('locally defined $log hides the $log injected by inject', function() { 110 | var $log; // declaration hides the one in window.$log created by inject 111 | 112 | bard.inject.bind(this)('$q', '$log'); 113 | 114 | expect($log).to.not.exist; 115 | expect(window.$log).to.exist; 116 | }); 117 | 118 | it('should set window.$log & window.foo when call inject("$log","block.foo")', function() { 119 | // register this ridiculous value for just this test 120 | module(function($provide) { 121 | $provide.value('block.foo', 'foo'); 122 | }); 123 | 124 | // Can inject a service with a dotted name! 125 | bard.inject(this, '$log', 'block.foo'); 126 | 127 | expect($log).to.exist; 128 | expect(foo).to.exist; 129 | expect(window.foo).to.exist; 130 | }); 131 | 132 | // This afterEach would fail because it is called BEFORE 133 | // the outer one created by bard to handle window cleaning 134 | // ----------------------------------------------------- 135 | // afterEach('Describe #2 afterEach', function() { 136 | // console.log('---Describe #2 afterEach'); 137 | // // Should have cleaned up after itself 138 | // expect(window.$log).to.not.exist; 139 | // expect(window.nutz).to.not.exist; 140 | // }); 141 | }); 142 | 143 | describe('(describe #4):', function() { 144 | it('window.$log and window.nutz should not exist', function() { 145 | expect(window.$log).to.not.exist; 146 | expect(window.nutz).to.not.exist; 147 | }); 148 | }); 149 | 150 | describe('(describe #5):', function() { 151 | beforeEach(function() { 152 | bard.log('bard.inject.spec (describe #5) beforeEach'); 153 | // register this ridiculous value for just this describe 154 | module(function($provide) { 155 | $provide.value('baz', 'baz'); 156 | }); 157 | 158 | bard.inject(this, 'baz'); // get baz in outer describe 159 | }); 160 | 161 | describe('in nested describe', function() { 162 | it('baz is available from parent describe', function() { 163 | expect(baz).to.exist; 164 | }); 165 | 166 | it('baz from inject() is same object as baz from direct injection', function() { 167 | 168 | inject(function(_baz_) { 169 | expect(baz).to.equal(_baz_); 170 | }); 171 | }); 172 | }); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /tests/bard.mockService.spec.js: -------------------------------------------------------------------------------- 1 | /* jshint -W117, -W030 */ 2 | describe('bard.mockService', function() { 3 | 'use strict'; 4 | 5 | var mockService = bard.mockService; 6 | var flush; 7 | var sandbox; 8 | 9 | beforeEach(function() { 10 | module(); 11 | bard.inject(this, '$q', '$rootScope', '$window'); 12 | sandbox = sinon.sandbox.create(); 13 | flush = function() { $rootScope.$apply(); }; 14 | }); 15 | 16 | afterEach(function() { 17 | sandbox.restore(); 18 | }); 19 | 20 | describe('when execute the "real" DoWork service described in the usage example', function() { 21 | var service; 22 | 23 | beforeEach(function() { 24 | service = getDoWorkService(); 25 | }); 26 | 27 | it('`doWork1` returns a resolved promise with the "real" results', function() { 28 | service.doWork1(1, 2) 29 | .then(function(results) { 30 | expect(results).to.deep.equal([1, 2]); 31 | }); 32 | flush(); 33 | }); 34 | 35 | it('`doWork2` calls alert and returns the "real" results', function() { 36 | var alert = sandbox.stub($window, 'alert'); 37 | bard.addGlobals(this, 'alert'); // because sinon adds it! 38 | var results = service.doWork2(); 39 | expect(results).to.equal('pointless'); 40 | expect(alert).to.have.been.calledWith('Hi there'); 41 | }); 42 | 43 | it('`doWork3` returns a resolved promise with the "real" results', function() { 44 | service.doWork3(1, 2) 45 | .then(function(results) { 46 | expect(results).to.deep.equal(['a1', 'a2']); 47 | }); 48 | flush(); 49 | }); 50 | 51 | it('`doWork4` returns the "real" results', function() { 52 | var results = service.doWork4(1, 2); 53 | expect(results).to.equal('Hi from doWork4'); 54 | }); 55 | 56 | it('does not have a `doWork5`', function() { 57 | expect(service).to.not.have.property('doWork5'); 58 | }); 59 | 60 | it('`doWorkProto` return the "real" results', function() { 61 | var results = service.doWorkProto(); 62 | expect(results).to.be.true; 63 | }); 64 | 65 | it('`isActive` should be true', function() { 66 | expect(service.isActive).to.be.true; 67 | }); 68 | }); 69 | 70 | describe('when mock the DoWork service as described in the usage example', function() { 71 | var service; 72 | 73 | beforeEach(function() { 74 | service = mockService(getDoWorkService(), 75 | { // config in the usage example 76 | doWork1: $q.when([{name: 'Bob'}, {name: 'Sally'}]), 77 | doWork2: undefined, 78 | doWork4: function() { return 'Now for a different kind of work';}, 79 | doWork5: $q.reject('bad boy!'), 80 | isActive: false, 81 | _default: $q.when([]) 82 | }); 83 | }); 84 | 85 | it('`doWork1` returns a resolved promise with the fake results', function() { 86 | service.doWork1(1, 2) 87 | .then(function(results) { 88 | expect(results).to.deep.equal([{name: 'Bob'}, {name: 'Sally'}]); 89 | }); 90 | // verify `doWork1` is a spy 91 | expect(service.doWork1).to.have.been.calledWith(1, 2); 92 | flush(); 93 | }); 94 | 95 | it('`doWork2` returns nothing', function() { 96 | var results = service.doWork2(1, 2); 97 | expect(results).to.not.be.defined; 98 | // verify `doWork2` is a spy 99 | expect(service.doWork2).to.have.been.calledWith(1, 2); 100 | }); 101 | 102 | it('`doWork3` returns a resolved promise with config._default (empty array)', function() { 103 | service.doWork3(1, 2).then(expectEmptyArray); 104 | // verify `doWork3` is a spy 105 | expect(service.doWork3).to.have.been.calledWith(1, 2); 106 | flush(); 107 | }); 108 | 109 | it('`doWork4` returns the fake results', function() { 110 | var results = service.doWork4(1, 2); 111 | expect(results).to.match(/different/); 112 | // verify `doWork4` is NOT a spy 113 | expect(service.doWork4).to.not.have.property('restore'); 114 | }); 115 | 116 | it('`doWork5` returns a rejected promise with the faked error', function() { 117 | service.doWork5() 118 | .then(function() { 119 | // Should not come here! 120 | expect('should have failed').to.be.true; 121 | }) 122 | .catch(function(err) { 123 | expect(err).to.match(/bad/); 124 | }); 125 | // verify `doWork5` is a spy 126 | expect(service.doWork5).to.have.been.called; 127 | flush(); 128 | }); 129 | 130 | it('`doWorkProto` returns `_default` value', function() { 131 | service.doWorkProto(1, 2).then(expectEmptyArray); 132 | // verify `doWork3` is a spy 133 | expect(service.doWorkProto).to.have.been.calledWith(1, 2); 134 | flush(); 135 | }); 136 | 137 | it('`isActive` should have changed to false', function() { 138 | expect(service.isActive).to.be.false; 139 | }); 140 | }); 141 | 142 | describe('when mock one async method of the DoWork service and default the rest', function() { 143 | // typical usage when mocking dataservice for a controller 144 | // mock the method(s) of interest; let the others do the minimum necessary 145 | var service; 146 | 147 | beforeEach(function() { 148 | service = mockService(getDoWorkService(), 149 | { // config in the usage example 150 | doWork1: $q.when([1, 2, 3]), 151 | _default: $q.when([]) 152 | }); 153 | }); 154 | 155 | it('`doWork1` returns a resolved promise with the fake results', function() { 156 | service.doWork1('foo').then(function(results) { 157 | expect(results).to.deep.equal([1, 2, 3]); 158 | }); 159 | flush(); 160 | }); 161 | 162 | it('`doWork2`-`doWork4` each return resolved promise with empty array', function() { 163 | service.doWork2('could').then(expectEmptyArray); 164 | service.doWork3('be').then(expectEmptyArray); 165 | service.doWork4('anything').then(expectEmptyArray); 166 | flush(); 167 | }); 168 | }); 169 | 170 | describe('when mock one async method of the DoWork service and omit _default', function() { 171 | var service; 172 | 173 | beforeEach(function() { 174 | service = mockService(getDoWorkService(), 175 | { // config in the usage example 176 | doWork1: $q.when([1, 2, 3]) 177 | }); 178 | }); 179 | 180 | it('`doWork1` returns a resolved promise with the fake results', function() { 181 | service.doWork1('foo').then(function(results) { 182 | expect(results).to.deep.equal([1, 2, 3]); 183 | }); 184 | flush(); 185 | }); 186 | 187 | it('`doWork2`-`doWork4` are stubbed to return nothing', function() { 188 | expect(service.doWork2('could')).to.not.be.defined; 189 | expect(service.doWork3('be')).to.not.be.defined; 190 | expect(service.doWork4('anything')).to.not.be.defined; 191 | // but they are stubbed 192 | expect(service.doWork2).to.have.been.calledWith('could'); 193 | expect(service.doWork3).to.have.been.calledWith('be'); 194 | expect(service.doWork4).to.have.been.calledWith('anything'); 195 | 196 | flush(); 197 | }); 198 | }); 199 | 200 | ///// helpers ///// 201 | 202 | // create the example DoWork service from bard.mockService usage doc 203 | function getDoWorkService() { 204 | var doWorkParent = { 205 | doWorkProto: function() { 206 | return true; 207 | } 208 | } 209 | 210 | var doWorkService = Object.create(doWorkParent) 211 | 212 | angular.extend(doWorkService, { 213 | doWork1: function doWork1(a, b) { 214 | return $q.when([].slice.apply(arguments)); 215 | }, 216 | doWork2: function doWork2() { 217 | $window.alert('Hi there'); // something we do NOT want to do in a test 218 | return 'pointless'; 219 | }, 220 | doWork3: function doWork3() { 221 | var args = [].slice.apply(arguments); 222 | // (1, 2) -> [a1, a2] 223 | var results = args.map(function(a) { return 'a' + a;}); 224 | return $q.when(results); 225 | }, 226 | doWork4: function() { 227 | return 'Hi from doWork4'; 228 | }, 229 | isActive: true 230 | }); 231 | 232 | return doWorkService; 233 | } 234 | 235 | function expectEmptyArray(results) { 236 | expect(results).to.deep.equal([]); 237 | } 238 | }); 239 | --------------------------------------------------------------------------------