├── .editorconfig ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── angular-test-runner.d.ts ├── angular-test-runner.js ├── karma.conf.js ├── package.json ├── src ├── actions.js ├── angular-test-runner.js ├── export.js └── server-runner.js └── test ├── debug-messages-test.js ├── http-server-test.js ├── internal └── attach-to-dom-test.js └── sample-test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | 17 | .idea/ 18 | *.iml -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.tgz 3 | 4 | test 5 | 6 | # WebStorm 7 | .idea 8 | 9 | .gitignore 10 | .npmignore 11 | 12 | node_modules 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.5 (7.12.2017) 2 | 3 | ### Allow to call `app.stop()` method without `attachToDocument: true`. 4 | 5 | ## 0.1.3 (16.11.2017) 6 | 7 | ### Wait as a last action in `perform` block 8 | 9 | We've fixed bug and now you can use `wait` operator as a last action in `perform` block 10 | 11 | ```typescript 12 | html.perform( 13 | click.in('.my-element'), 14 | wait(200) 15 | ); 16 | ``` 17 | 18 | ## 0.1.0 (31.10.2017) 19 | 20 | ### Attach to DOM 21 | 22 | If you want to test component which using for example `angular.element()` function (or library, eg. ngDialog) 23 | you can add `attachToDocument` configuration flag. 24 | 25 | ```typescript 26 | beforeEach(() => { 27 | app.run(['moduleName'], {attachToDocument : true}); 28 | } 29 | ``` 30 | 31 | Because we need to detach component from document after the test you need to call `app.stop()` function as well 32 | 33 | ```typescript 34 | afterEach(() => { 35 | app.stop(); 36 | } 37 | ``` 38 | 39 | [More complex example](https://github.com/Pragmatists/angular-test-runner/blob/master/test/sample-test.js#L242) 40 | 41 | ## 0.0.5 (27.10.2017) 42 | 43 | ### Blur action 44 | 45 | Now we can use blur action on element 46 | 47 | ```typescript 48 | html.perform( 49 | blur.from('input.name') 50 | ); 51 | ``` 52 | 53 | 54 | ## 0.0.1 (29.03.2017) 55 | 56 | ### Accept RegExp as http request url 57 | 58 | Now we can pass url to server http request as RegExp instead of String 59 | 60 | ```typescript 61 | server.get(new RegExp('/api/user/(\\d+)/address'), res => res.sendStatus(200)); 62 | ``` 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Pragmatists (http://pragmatists.com) 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 | # angular-test-runner 2 | 3 | [![Join the chat at https://gitter.im/angular-test-runner/Lobby](https://badges.gitter.im/angular-test-runner/Lobby.svg)](https://gitter.im/angular-test-runner/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | [![npm version](https://img.shields.io/npm/v/angular-test-runner.svg?style=flat-square)](https://www.npmjs.com/package/angular-test-runner) 5 | 6 | Micro testing library that allows you to practice TDD in AngularJs applications. 7 | 8 | ## Installation 9 | To install it, type: 10 | 11 | $ npm install angular-test-runner --save-dev 12 | 13 | ## Configuration 14 | `angular-test-runner` has depedency on [`jasmine-jquery`](https://github.com/bessdsv/karma-jasmine-jquery) and [`karma-jasmine-jquery`](https://github.com/bessdsv/karma-jasmine-jquery) so it is required to add it as well. 15 | 16 | ```js 17 | // karma.conf.js 18 | module.exports = function(config) { 19 | config.set({ 20 | frameworks: ['jasmine-jquery', 'jasmine'], 21 | 22 | plugins: ['karma-jasmine-jquery'] 23 | 24 | // ... 25 | }) 26 | } 27 | ``` 28 | ## Replacement for ngMock 29 | **angular-test-runner** was created as a replacement of "official" Angular testing library: **ngMock**. It was designed to address following shortcommings of **ngMock**: 30 | 1. different style of testing for each component type (controller, directive, service, etc.), 31 | 2. long and boilerplate-rich setup of test, 32 | 3. difficult testing of html templates and no support for interacting with DOM elements, 33 | 4. focusing on testing objects in isolation instead of testing coherent set of behaviours (white-box testing) 34 | 5. [obscure tests with irrelevant informations](http://xunitpatterns.com/Obscure%20Test.html#Irrelevant%20Information) exposing implentation details, especially in context of HTTP communication (need of `$http.flush()`) and DOM interaction (need of `$scope.$digest()`) 35 | 36 | Therefore **angular-test-runner** tries to address those issues by: 37 | 1. providing uniform abstraction and consistent API for testing Angular parts, regardless of their type (contoller, directive, component, filter, etc.), 38 | 2. providing higher confidence by excercising html templates and interacting with real DOM elements (black-box testing), 39 | 3. promoting fine grained, self-encapsulated modules by enforcing testing at a module level instead of testing controllers, services, directives in isolation, 40 | 4. avioding mocks in favour of fakes for HTTP communication, use synchronous HTTP stubbing by default (no need for `$http.flush()`) and asynchronous on demand, 41 | 5. introducing compact, readable and declarative programming interface by introducing fluent interface. 42 | 43 | 44 | ## Example 45 | 46 | Following example presents tests for simple counter component. 47 | 48 | ``` javascript 49 | var test = require('angular-test-runner'); 50 | var { click, expectElement } = test.actions; 51 | 52 | describe('counter component', () => { 53 | 54 | var app, server, html; 55 | 56 | beforeEach(() => { 57 | app = test.app(['counterApp']); 58 | server = test.http(); 59 | }); 60 | 61 | beforeEach(() => { 62 | html = app.runHtml('', {start: 0}); 63 | }); 64 | 65 | it('should render counter value', () => { 66 | 67 | html.verify( 68 | expectElement('#counter').toHaveText('0') 69 | ); 70 | 71 | }); 72 | 73 | it('should increment counter value by 1', () => { 74 | 75 | html.perform( 76 | click.in('button#increment') 77 | ); 78 | 79 | html.verify( 80 | expectElement('#counter').toHaveText('1') 81 | ); 82 | }); 83 | 84 | it('should increment counter value by 1', () => { 85 | 86 | var jsonSentToServer; 87 | 88 | server.post('/counter/increment', (req) => { 89 | jsonSentToServer = req.body(); 90 | req.sendStatus(200); 91 | }); 92 | 93 | html.perform( 94 | click.in('button#increment') 95 | ); 96 | 97 | html.verify( 98 | () => expect(jsonSentToServer).toEqual({ counter: 1 }) 99 | ); 100 | }); 101 | 102 | }); 103 | ``` 104 | 105 | ### Comparision to ngMock 106 | 107 | Below you can compare **angular-test-runner** style of testing to traditional **ngMock** way. 108 | Following example was taken from [Official Angular Developers Guide](https://docs.angularjs.org/guide/component#unit-testing-component-controllers): 109 | 110 | ![ngMock vs angular-test-runner](http://pragmatists.pl/img/ngMock_vs_angular_test_runner.png) 111 | 112 | See more [examples](https://github.com/Pragmatists/angular-test-runner/blob/master/test/sample-test.js) 113 | 114 | ### Why not Protractor 115 | 116 | While **Protractor** is focused on end-to-end testing Angular application as a whole (from a browser perspective), 117 | **angular-test-runner** focuses on testing coherent parts of application by excerising selected components in isolation. 118 | 119 | Unlike **Protractor** in **angular-test-runner** you don't have to start your backend server nor host your html files on localhost so it is accessible to Selenium. Therefore test runner setup and configuration is much more simple. 120 | Moreover you are not forced to test entire pages at once, but you can exercise only a tiny fragments of your html (e.g. single directive, component or filter a.k.a. pipe). 121 | 122 | ## Features 123 | * readable performing [actions](https://github.com/Pragmatists/angular-test-runner/wiki/Testing-DOM-interactions), e.g. clicking on elements, typing into inputs etc. 124 | * easy request and response [server stubbing](https://github.com/Pragmatists/angular-test-runner/wiki/Testing-HTTP-interactions) 125 | * simplified testing of code with [async operations](https://github.com/Pragmatists/angular-test-runner/wiki/Testing-HTTP-interactions#async-mode) 126 | * easy to [write asserts](https://github.com/Pragmatists/angular-test-runner/wiki/Testing-DOM-interactions#expectelementelement) concerning html elements 127 | -------------------------------------------------------------------------------- /angular-test-runner.d.ts: -------------------------------------------------------------------------------- 1 | declare interface AngularTestRunner { 2 | app: (modules: string[]) => angularTestRunner.ITestRunnerApp; 3 | http: (settings?: angularTestRunner.ITestRunnerHttpSettings) => angularTestRunner.ITestRunnerHttp; 4 | actions: angularTestRunner.ITestRunnerActions; 5 | } 6 | 7 | declare namespace angularTestRunner { 8 | 9 | interface ITestRunnerApp { 10 | stop: () => void; 11 | runHtml: (html: string, scope?: any) => ITestHtml; 12 | run: (location: string, scope?: any) => ITestHtml; 13 | } 14 | 15 | interface ITestRunnerHttpSettings { 16 | autoRespond?: boolean; 17 | respondImmediately?: boolean; 18 | respondAfter?: number; 19 | } 20 | 21 | interface ITestHtml { 22 | perform: (...actions: IAction[]) => void; 23 | verify: (...actions: IVerificationAction[]) => void; 24 | destroy: () => void; 25 | } 26 | 27 | interface IHttpRequest { 28 | body: () => any; 29 | query: () => any; 30 | header: (name: string) => string; 31 | sendJson: (json: any) => void; 32 | sendStatus: (status: number, json?: any) => void 33 | } 34 | 35 | type IHttpHandler = (request: IHttpRequest) => any; 36 | type IHttpEndpoint = (url: string | RegExp, handler: IHttpHandler) => any; 37 | 38 | interface ITestRunnerHttp { 39 | 40 | get: IHttpEndpoint; 41 | post: IHttpEndpoint; 42 | put: IHttpEndpoint; 43 | delete: IHttpEndpoint; 44 | stop: () => any; 45 | respond: () => any; 46 | 47 | } 48 | 49 | type IAction = (JQuery) => any; 50 | 51 | interface IAfterAction { 52 | after?: (number) => IAction; 53 | } 54 | 55 | type IVerificationAction = IAction & IAfterAction; 56 | 57 | interface IInAction { 58 | in: (string) => IAction & IAfterAction; 59 | } 60 | 61 | type IKeyAction = (key: number) => IAction & IInAction; 62 | 63 | interface ITestRunnerActions { 64 | 65 | click: IAction & IInAction; 66 | type: (text: string) => IAction & IInAction; 67 | keydown: IKeyAction; 68 | keypress: IKeyAction; 69 | keyup: IKeyAction; 70 | mouseover: IAction & IInAction; 71 | mouseleave: IAction & IInAction; 72 | wait: (delay: number) => IAction; 73 | apply: IAction; 74 | navigateTo: (url: string) => IAction; 75 | expectElement: (selector: string) => Matchers; 76 | listenTo: (eventName: string, handler: (data: any) => void) => IAction; 77 | publishEvent: (eventName: string, data: any) => IAction; 78 | } 79 | 80 | interface Matchers { 81 | 82 | /** 83 | * Check if DOM element has class. 84 | * 85 | * @param className Name of the class to check. 86 | * 87 | * @example 88 | * // returns true 89 | * expect($('
')).toHaveClass("some-class") 90 | */ 91 | toHaveClass(className: string): IVerificationAction; 92 | 93 | /** 94 | * Check if DOM element has the given CSS properties. 95 | * 96 | * @param css Object containing the properties (and values) to check. 97 | * 98 | * @example 99 | * // returns true 100 | * expect($('
')).toHaveCss({display: "none", margin: "10px"}) 101 | * 102 | * @example 103 | * // returns true 104 | * expect($('
')).toHaveCss({margin: "10px"}) 105 | */ 106 | toHaveCss(css: any): IVerificationAction; 107 | 108 | /** 109 | * Checks if DOM element is visible. 110 | * Elements are considered visible if they consume space in the document. Visible elements have a width or height that is greater than zero. 111 | */ 112 | toBeVisible(): IVerificationAction; 113 | /** 114 | * Check if DOM element is hidden. 115 | * Elements can be hidden for several reasons: 116 | * - They have a CSS display value of none ; 117 | * - They are form elements with type equal to hidden. 118 | * - Their width and height are explicitly set to 0. 119 | * - An ancestor element is hidden, so the element is not shown on the page. 120 | */ 121 | toBeHidden(): IVerificationAction; 122 | 123 | /** 124 | * Only for tags that have checked attribute 125 | * 126 | * @example 127 | * // returns true 128 | * expect($('')).toBeSelected() 129 | */ 130 | toBeSelected(): IVerificationAction; 131 | 132 | /** 133 | * Only for tags that have checked attribute 134 | * @example 135 | * // returns true 136 | * expect($('')).toBeChecked() 137 | */ 138 | toBeChecked(): IVerificationAction; 139 | 140 | /** 141 | * Checks for child DOM elements or text 142 | */ 143 | toBeEmpty(): IVerificationAction; 144 | 145 | /** 146 | * Checks if element exists in or out the DOM. 147 | */ 148 | toExist(): IVerificationAction; 149 | 150 | /** 151 | * Checks if array has the given length. 152 | * 153 | * @param length Expected length 154 | */ 155 | toHaveLength(length: number): IVerificationAction; 156 | 157 | /** 158 | * Check if DOM element contains an attribute and, optionally, if the value of the attribute is equal to the expected one. 159 | * 160 | * @param attributeName Name of the attribute to check 161 | * @param expectedAttributeValue Expected attribute value 162 | */ 163 | toHaveAttr(attributeName: string, expectedAttributeValue?: any): IVerificationAction; 164 | 165 | /** 166 | * Check if DOM element contains a property and, optionally, if the value of the property is equal to the expected one. 167 | * 168 | * @param propertyName Property name to check 169 | * @param expectedPropertyValue Expected property value 170 | */ 171 | toHaveProp(propertyName: string, expectedPropertyValue?: any): IVerificationAction; 172 | 173 | /** 174 | * Check if DOM element has the given Id 175 | * 176 | * @param Id Expected identifier 177 | */ 178 | toHaveId(id: string): IVerificationAction; 179 | 180 | /** 181 | * Check if DOM element has the specified HTML. 182 | * 183 | * @example 184 | * // returns true 185 | * expect($('
')).toHaveHtml('') 186 | */ 187 | toHaveHtml(html: string): IVerificationAction; 188 | 189 | /** 190 | * Check if DOM element contains the specified HTML. 191 | * 192 | * @example 193 | * // returns true 194 | * expect($('

header

')).toContainHtml('') 195 | */ 196 | toContainHtml(html: string): IVerificationAction; 197 | 198 | /** 199 | * Check if DOM element has the given Text. 200 | * @param text Accepts a string or regular expression 201 | * 202 | * @example 203 | * // returns true 204 | * expect($('
some text
')).toHaveText('some text') 205 | */ 206 | toHaveText(text: string): IVerificationAction; 207 | /** 208 | * Check if DOM element contains the specified text. 209 | * 210 | * @example 211 | * // returns true 212 | * expect($('

header

')).toContainText('header') 213 | */ 214 | toContainText(text: string): IVerificationAction; 215 | 216 | /** 217 | * Check if DOM element has the given value. 218 | * This can only be applied for element on with jQuery val() can be called. 219 | * 220 | * @example 221 | * // returns true 222 | * expect($('')).toHaveValue('some text') 223 | */ 224 | toHaveValue(value: string): IVerificationAction; 225 | 226 | /** 227 | * Check if DOM element has the given data. 228 | * This can only be applied for element on with jQuery data(key) can be called. 229 | * 230 | */ 231 | toHaveData(key: string, expectedValue: string): IVerificationAction; 232 | toBe(selector: JQuery): IVerificationAction; 233 | 234 | /** 235 | * Check if DOM element is matched by the given selector. 236 | * 237 | * @example 238 | * // returns true 239 | * expect($('
')).toContain('some-class') 240 | */ 241 | toContain(selector: JQuery): IVerificationAction; 242 | 243 | /** 244 | * Check if DOM element exists inside the given parent element. 245 | * 246 | * @example 247 | * // returns true 248 | * expect($('
')).toContainElement('span.some-class') 249 | */ 250 | toContainElement(selector: string): IVerificationAction; 251 | 252 | /** 253 | * Check to see if the set of matched elements matches the given selector 254 | * 255 | * @example 256 | * expect($('').addClass('js-something')).toBeMatchedBy('.js-something') 257 | * 258 | * @returns {Boolean} true if DOM contains the element 259 | */ 260 | toBeMatchedBy(selector: string): IVerificationAction; 261 | 262 | /** 263 | * Only for tags that have disabled attribute 264 | * @example 265 | * // returns true 266 | * expect('').toBeDisabled() 267 | */ 268 | toBeDisabled(): IVerificationAction; 269 | 270 | /** 271 | * Check if DOM element is focused 272 | * @example 273 | * // returns true 274 | * expect($('').focus()).toBeFocused() 275 | */ 276 | toBeFocused(): IVerificationAction; 277 | 278 | /** 279 | * Checks if DOM element handles event. 280 | * 281 | * @example 282 | * // returns true 283 | * expect($form).toHandle("submit") 284 | */ 285 | toHandle(eventName: string): IVerificationAction; 286 | 287 | /** 288 | * Assigns a callback to an event of the DOM element. 289 | * 290 | * @param eventName Name of the event to assign the callback to. 291 | * @param eventHandler Callback function to be assigned. 292 | * 293 | * @example 294 | * expect($form).toHandleWith("submit", yourSubmitCallback) 295 | */ 296 | toHandleWith(eventName: string, eventHandler: (...params: any[]) => any): IVerificationAction; 297 | 298 | /** 299 | * Checks if event was triggered. 300 | */ 301 | toHaveBeenTriggered(): IVerificationAction; 302 | 303 | /** 304 | * Checks if the event has been triggered on selector. 305 | * @param selector Selector that should have triggered the event. 306 | */ 307 | toHaveBeenTriggeredOn(selector: string): IVerificationAction; 308 | 309 | /** 310 | * Checks if the event has been triggered on selector. 311 | * @param selector Selector that should have triggered the event. 312 | * @param args Extra arguments to be passed to jQuery events functions. 313 | */ 314 | toHaveBeenTriggeredOnAndWith(selector: string, ...args: any[]): IVerificationAction; 315 | 316 | /** 317 | * Checks if event propagation has been prevented. 318 | */ 319 | toHaveBeenPrevented(): IVerificationAction; 320 | 321 | /** 322 | * Checks if event propagation has been prevented on element with selector. 323 | * 324 | * @param selector Selector that should have prevented the event. 325 | */ 326 | toHaveBeenPreventedOn(selector: string): IVerificationAction; 327 | 328 | /** 329 | * Checks if event propagation has been stopped. 330 | * 331 | * @example 332 | * // returns true 333 | * var spyEvent = spyOnEvent('#some_element', 'click') 334 | * $('#some_element').click(function (event){event.stopPropagation();}) 335 | * $('#some_element').click() 336 | * expect(spyEvent).toHaveBeenStopped() 337 | */ 338 | toHaveBeenStopped(): IVerificationAction; 339 | 340 | /** 341 | * Checks if event propagation has been stopped by an element with the given selector. 342 | * @param selector Selector of the element that should have stopped the event propagation. 343 | * 344 | * @example 345 | * // returns true 346 | * $('#some_element').click(function (event){event.stopPropagation();}) 347 | * $('#some_element').click() 348 | * expect('click').toHaveBeenStoppedOn('#some_element') 349 | */ 350 | toHaveBeenStoppedOn(selector: string): IVerificationAction; 351 | 352 | /** 353 | * Checks to see if the matched element is attached to the DOM. 354 | * @example 355 | * expect($('#id-name')[0]).toBeInDOM() 356 | */ 357 | toBeInDOM(): IVerificationAction; 358 | 359 | not: Matchers; 360 | } 361 | 362 | interface JQuery { 363 | find(element: any): JQuery; 364 | find(obj: JQuery): JQuery; 365 | } 366 | } 367 | 368 | declare const angularTestRunner: AngularTestRunner; 369 | 370 | export = angularTestRunner; 371 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | basePath: '', 4 | frameworks: ['jasmine-jquery', 'jasmine', 'sinon'], 5 | files: [ 6 | 'node_modules/lodash/lodash.js', 7 | 'node_modules/jquery/dist/jquery.js', 8 | 'node_modules/angular/angular.min.js', 9 | 'angular-test-runner.js', 10 | 'test/**/*.js' 11 | ], 12 | exclude: [], 13 | preprocessors: { 14 | '**/*.html': ['ng-html2js'] 15 | }, 16 | reporters: ['dots'], 17 | port: 9876, 18 | colors: true, 19 | logLevel: config.LOG_INFO, 20 | autoWatch: true, 21 | singleRun: false, 22 | browsers: ['PhantomJS'] 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-test-runner", 3 | "version": "0.1.5", 4 | "description": "Test Angular stuff without ngMock", 5 | "main": "angular-test-runner.js", 6 | "typings": "angular-test-runner.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/Pragmatists/angular-test-runner" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/Pragmatists/angular-test-runner/issues" 13 | }, 14 | "keywords": [ 15 | "angular", 16 | "js", 17 | "javascript", 18 | "testing", 19 | "test", 20 | "tdd" 21 | ], 22 | "license": "MIT", 23 | "scripts": { 24 | "test": "karma start karma.conf.js", 25 | "dist": "browserify src/export.js --standalone testRunner -o angular-test-runner.js" 26 | }, 27 | "dependencies": { 28 | "body-parser": "^1.4.3", 29 | "jquery": "^2.2.4", 30 | "lodash": "^4.13.1", 31 | "sinon": "^1.17.4", 32 | "karma-jasmine-jquery": "^0.1.1", 33 | "jasmine-jquery": "^2.1.1" 34 | }, 35 | "devDependencies": { 36 | "angular": "1.6.0", 37 | "browserify": "^13.0.1", 38 | "jasmine-core": "^2.4.1", 39 | "jasmine-jquery": "^2.1.1", 40 | "karma": "^0.13.22", 41 | "karma-chrome-launcher": "^1.0.1", 42 | "karma-html2js-preprocessor": "^1.0.0", 43 | "karma-jasmine": "^1.0.2", 44 | "karma-ng-html2js-preprocessor": "^1.0.0", 45 | "karma-phantomjs-launcher": "^1.0.0", 46 | "karma-sinon": "^1.0.5", 47 | "phantomjs-prebuilt": "^2.1.7", 48 | "sinon": "^1.17.4" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | var jQuery = require('jquery'); 2 | var _ = require('lodash'); 3 | 4 | function assertSingle(fn) { 5 | var name = fn.name; 6 | return function ($el) { 7 | var found = $el.size(); 8 | if (found === 0) { 9 | throw new Error('Could not find ' + $el.selector + ' element for ' + name + ' action'); 10 | } 11 | if (found > 1) { 12 | throw new Error('Found multiple ' + $el.selector + ' elements for ' + name + ' action'); 13 | } 14 | return fn.apply(this, _.toArray(arguments)); 15 | }; 16 | } 17 | 18 | function type(text) { 19 | return withIn(assertSingle(function type($el) { 20 | $el.val(text); 21 | $el.change(); 22 | })); 23 | } 24 | 25 | function keypress(key) { 26 | return withIn(assertSingle(function keypress($el) { 27 | $el.trigger(jQuery.Event('keypress', {keyCode: key, which: key})); 28 | })); 29 | } 30 | 31 | function keydown(key) { 32 | return withIn(assertSingle(function keydown($el) { 33 | $el.trigger(jQuery.Event('keydown', {keyCode: key, which: key})); 34 | })); 35 | } 36 | 37 | function keyup(key) { 38 | return withIn(assertSingle(function keyup($el) { 39 | $el.trigger(jQuery.Event('keyup', {keyCode: key, which: key})); 40 | })); 41 | } 42 | 43 | function blur($el) { 44 | $el.trigger(jQuery.Event('blur')); 45 | } 46 | 47 | function mouseover($el) { 48 | $el.trigger(jQuery.Event('mouseover')); 49 | } 50 | 51 | function mouseleave($el) { 52 | $el.trigger(jQuery.Event('mouseleave')); 53 | } 54 | 55 | function apply($el) { 56 | var scope = angular.element($el).scope(); 57 | scope.$apply(); 58 | } 59 | 60 | function click($el) { 61 | $el.click(); 62 | } 63 | 64 | function listenTo(event, handler) { 65 | return function ($el) { 66 | findScope($el).$on(event, function ($event, data) { 67 | handler(data); 68 | }); 69 | }; 70 | } 71 | 72 | function publishEvent(event, data) { 73 | return function ($el) { 74 | findScope($el).$broadcast(event, data); 75 | }; 76 | } 77 | 78 | function findScope(el) { 79 | if (scopeFor(el)) { 80 | return scopeFor(el); 81 | } 82 | var children = el.children(); 83 | for (var i = 0; i < children.length; i++) { 84 | var scope = scopeFor(children[i]); 85 | if (scope) { 86 | return scope; 87 | } 88 | } 89 | throw new Error('Cannot find scope on top level children of root element', el); 90 | 91 | function scopeFor(element) { 92 | return angular.element(element).scope(); 93 | } 94 | } 95 | 96 | function wait(timeout) { 97 | return function () { 98 | return { 99 | then: function (callback) { 100 | setTimeout(callback, timeout || 0); 101 | } 102 | }; 103 | }; 104 | } 105 | 106 | function navigateTo(url) { 107 | return withAfter(function ($el) { 108 | angular 109 | .element('') 110 | .appendTo($el) 111 | .click() 112 | .remove(); 113 | }); 114 | } 115 | 116 | function withIn(fn) { 117 | fn.in = selectWithAfter(fn); 118 | return withAfter(fn); 119 | } 120 | 121 | function withFrom(fn) { 122 | fn.from = selectWithAfter(fn); 123 | return withAfter(fn); 124 | } 125 | 126 | function selectWithAfter(fn) { 127 | return function (selector) { 128 | return withAfter(function ($el) { 129 | fn($el.find(selector)); 130 | }); 131 | } 132 | } 133 | 134 | function withAfter(fn) { 135 | fn.after = function (timeout) { 136 | return function ($el) { 137 | var callback = _.noop; 138 | setTimeout(function () { 139 | fn($el); 140 | callback(); 141 | }, timeout); 142 | 143 | return { 144 | then: function (cb) { 145 | callback = cb; 146 | } 147 | } 148 | }; 149 | }; 150 | return fn; 151 | } 152 | 153 | function expectElement(selector) { 154 | 155 | var perform = { 156 | not: {} 157 | }; 158 | 159 | var jasmine = expect(''); 160 | 161 | _(jasmine) 162 | .map(function (fn, name) { 163 | return {fn: fn, name: name}; 164 | }) 165 | .filter(function (fn) { 166 | return fn.name.indexOf('to') === 0 && _.isFunction(fn.fn); 167 | }) 168 | .forEach(function (fn) { 169 | perform[fn.name] = function () { 170 | var args = _.toArray(arguments); 171 | return withAfter(function ($el) { 172 | var x = $el.find(selector); 173 | x.toString = function () { 174 | return '[\n\t' + (x[0] ? x[0].outerHTML : '(no elements matched)') + '\n]'; 175 | }; 176 | var actual = expect(x); 177 | var matcher = actual[fn.name]; 178 | var result = matcher.apply(actual, args); 179 | }); 180 | }; 181 | perform.not[fn.name] = function () { 182 | var args = _.toArray(arguments); 183 | return withAfter(function ($el) { 184 | var x = $el.find(selector); 185 | x.toString = function () { 186 | return '[\n\t' + (x[0] ? x[0].outerHTML : '(no elements matched)') + '\n]'; 187 | }; 188 | var actual = expect(x).not; 189 | var matcher = actual[fn.name]; 190 | var result = matcher.apply(actual, args); 191 | }); 192 | }; 193 | }); 194 | 195 | return perform; 196 | } 197 | 198 | module.exports = { 199 | click: withIn(assertSingle(click)), 200 | wait: wait, 201 | type: type, 202 | keypress: keypress, 203 | keyup: keyup, 204 | keydown: keydown, 205 | mouseover: withIn(assertSingle(mouseover)), 206 | mouseleave: withIn(assertSingle(mouseleave)), 207 | blur: withFrom(assertSingle(blur)), 208 | navigateTo: navigateTo, 209 | apply: apply, 210 | expectElement: expectElement, 211 | listenTo: listenTo, 212 | publishEvent: publishEvent 213 | }; 214 | 215 | -------------------------------------------------------------------------------- /src/angular-test-runner.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | module.exports = app; 4 | 5 | function app(modules, config) { 6 | 7 | const appClassname = 'ng-app'; 8 | const defaultConfig = {attachToDocument: false}; 9 | var appConfig = _.defaults(config, defaultConfig); 10 | var body = angular.element(window.document.body); 11 | 12 | return { 13 | run: _.partial(run, _, _, true), 14 | runHtml: _.partial(run, _, _, false), 15 | stop: stop 16 | }; 17 | 18 | function stop() { 19 | var children = body.children(); 20 | for (var i = 0; i < children.length; i++) { 21 | var element = angular.element(children[i])[0]; 22 | if (element.tagName !== 'SCRIPT') { 23 | element.remove(); 24 | } 25 | } 26 | } 27 | 28 | function run(html, scope, isUrl) { 29 | 30 | var element = angular.element('
'); 31 | 32 | var modulesToLoad = modules; 33 | if (isUrl) { 34 | modulesToLoad = modulesToLoad.concat(html); 35 | } 36 | 37 | angular.module('test-app', modulesToLoad) 38 | .directive('testApp', function () { 39 | var d = { 40 | restrict: 'A' 41 | }; 42 | if (isUrl) { 43 | d.templateUrl = html; 44 | } else { 45 | d.template = html; 46 | } 47 | return d; 48 | }) 49 | .run(function ($rootScope) { 50 | _.assign($rootScope, scope); 51 | }); 52 | 53 | var actions = []; 54 | var injector = angular.bootstrap(element, ['test-app']); 55 | 56 | attachApplicationToDocument(); 57 | 58 | return { 59 | perform: perform, 60 | verify: perform, 61 | destroy: destroy, 62 | stop: stop 63 | }; 64 | 65 | function attachApplicationToDocument() { 66 | if (appConfig.attachToDocument) { 67 | body.append(element); 68 | } 69 | } 70 | 71 | function destroy() { 72 | injector.get('$rootScope').$destroy(); 73 | } 74 | 75 | function execute() { 76 | var action = takeNextAction(); 77 | if (lastAction()) { 78 | doScopeApply(); 79 | return; 80 | } 81 | var result = executeAction(); 82 | if (isPromise(result)) { 83 | result.then(execute); 84 | } else { 85 | execute(); 86 | } 87 | 88 | function doScopeApply() { 89 | var scope = angular.element(element).scope(); 90 | if (scope) { 91 | scope.$apply(); 92 | } 93 | } 94 | 95 | function executeAction() { 96 | return action(appConfig.attachToDocument ? body : element); 97 | } 98 | 99 | function lastAction() { 100 | return !action; 101 | } 102 | 103 | function takeNextAction() { 104 | return actions.shift(); 105 | } 106 | } 107 | 108 | function isPromise(promise) { 109 | return promise && typeof promise.then == 'function'; 110 | } 111 | 112 | function push(action) { 113 | actions.push(action); 114 | } 115 | 116 | function perform() { 117 | 118 | var wasEmpty = !actions.length; 119 | 120 | _([arguments]) 121 | .union([emptyAction]) 122 | .flattenDeep() 123 | .each(push); 124 | 125 | if (wasEmpty) { 126 | execute(); 127 | } 128 | 129 | function emptyAction() { 130 | } 131 | } 132 | } 133 | 134 | } 135 | 136 | -------------------------------------------------------------------------------- /src/export.js: -------------------------------------------------------------------------------- 1 | var app = require('./angular-test-runner.js'); 2 | var http = require('./server-runner.js'); 3 | var actions = require('./actions.js'); 4 | 5 | module.exports = { 6 | app: app, 7 | http: http, 8 | actions: actions 9 | }; 10 | -------------------------------------------------------------------------------- /src/server-runner.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var sinon = require('sinon'); 3 | 4 | module.exports = http; 5 | 6 | function http(config) { 7 | 8 | var defaultConfig = { 9 | autoRespond: true, 10 | respondImmediately: true 11 | }; 12 | 13 | var server = sinon.fakeServer.create(_.defaults(config, defaultConfig)); 14 | 15 | var that = { 16 | post: method('POST'), 17 | get: method('GET'), 18 | delete: method('DELETE'), 19 | put: method('PUT'), 20 | respond: function () { 21 | server.respond(); 22 | }, 23 | stop: function () { 24 | server.restore(); 25 | } 26 | }; 27 | 28 | return that; 29 | 30 | function method(type) { 31 | return function (url, handler) { 32 | server.respondWith(type, url, function (req) { 33 | handler(wrap(req)); 34 | }); 35 | return that; 36 | }; 37 | } 38 | } 39 | 40 | function wrap(req) { 41 | return { 42 | body: function () { 43 | return JSON.parse(req.requestBody); 44 | }, 45 | query: function () { 46 | var query = req.url.split('#')[0].split('?')[1]; 47 | return _(query) 48 | .split('&') 49 | .map(_.partial(_.split, _, '=', 2)) 50 | .fromPairs() 51 | .mapValues(decodeURIComponent) 52 | .value(); 53 | }, 54 | header: function (name) { 55 | return req.requestHeaders[name]; 56 | }, 57 | sendJson: function (json) { 58 | req.respond(200, {'Content-Type': 'application/json'}, JSON.stringify(json)); 59 | }, 60 | sendStatus: function (status, json) { 61 | req.respond(status, {'Content-Type': 'application/json'}, JSON.stringify(json || {})); 62 | } 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /test/debug-messages-test.js: -------------------------------------------------------------------------------- 1 | describe('debug messages test', function () { 2 | 3 | angular.module('debug-app', []); 4 | 5 | var app, html; 6 | var actions = testRunner.actions; 7 | 8 | beforeEach(function () { 9 | app = testRunner.app(['debug-app']); 10 | }); 11 | 12 | var singleElementActions = { 13 | click: actions.click, 14 | type: actions.type('text'), 15 | keypress: actions.keypress(13), 16 | keyup: actions.keyup(13), 17 | keydown: actions.keydown(13) 18 | }; 19 | 20 | var singleElementActionsWithFrom = { 21 | blur: actions.blur 22 | }; 23 | 24 | beforeEach(function () { 25 | // given: 26 | html = app.runHtml('
'); 27 | }); 28 | 29 | _(singleElementActions).each(function (action, name) { 30 | 31 | it('fails meaningfully if target for a "' + name + '" action cannot be find', function () { 32 | 33 | // when: 34 | try { 35 | html.perform( 36 | action.in('#missing') 37 | ); 38 | fail('expected exception due to #missing not found'); 39 | 40 | } catch (e) { 41 | // then: 42 | expect(e).toEqual(new Error('Could not find #missing element for ' + name + ' action')); 43 | } 44 | 45 | }); 46 | 47 | it('fails meaningfully if target for a "' + name + '" action is ambiguous', function () { 48 | 49 | // when: 50 | try { 51 | html.perform( 52 | action.in('.ambiguous') 53 | ); 54 | fail('expected exception due to multiple .ambiguous found'); 55 | 56 | } catch (e) { 57 | // then: 58 | expect(e).toEqual(new Error('Found multiple .ambiguous elements for ' + name + ' action')); 59 | } 60 | 61 | }); 62 | 63 | }); 64 | 65 | _(singleElementActionsWithFrom).each(function (action, name) { 66 | 67 | it('fails meaningfully if target for a "' + name + '" action cannot be find', function () { 68 | 69 | // when: 70 | try { 71 | html.perform( 72 | action.from('#missing') 73 | ); 74 | fail('expected exception due to #missing not found'); 75 | 76 | } catch (e) { 77 | // then: 78 | expect(e).toEqual(new Error('Could not find #missing element for ' + name + ' action')); 79 | } 80 | 81 | }); 82 | 83 | it('fails meaningfully if target for a "' + name + '" action is ambiguous', function () { 84 | 85 | // when: 86 | try { 87 | html.perform( 88 | action.from('.ambiguous') 89 | ); 90 | fail('expected exception due to multiple .ambiguous found'); 91 | 92 | } catch (e) { 93 | // then: 94 | expect(e).toEqual(new Error('Found multiple .ambiguous elements for ' + name + ' action')); 95 | } 96 | 97 | }); 98 | 99 | }); 100 | 101 | }); 102 | -------------------------------------------------------------------------------- /test/http-server-test.js: -------------------------------------------------------------------------------- 1 | describe('server', function () { 2 | 3 | angular.module('async-server-app', []) 4 | .directive('greeting', function () { 5 | return { 6 | template: ['
{{ message }}
Send'].join(), 7 | scope: { 8 | name: '=' 9 | }, 10 | controllerAs: 'vm', 11 | controller: function ($http, $scope) { 12 | $scope.message = 'initial'; 13 | $http.get('/greeting') 14 | .then(function (response) { 15 | var json = response.data; 16 | $scope.message = response.data.message; 17 | }) 18 | .catch(function (response) { 19 | $scope.message = response.status; 20 | }); 21 | 22 | this.sendGreetings = function () { 23 | $http.post('/greeting?to=Jane%20Doe&when=now', {who: 'John'}, 24 | { 25 | headers: { 26 | 'x-my-header': 'a-my-value' 27 | } 28 | }); 29 | }; 30 | } 31 | }; 32 | }); 33 | 34 | var server, app; 35 | var expectElement = testRunner.actions.expectElement; 36 | var click = testRunner.actions.click; 37 | 38 | beforeEach(function () { 39 | app = testRunner.app(['async-server-app']); 40 | }); 41 | 42 | 43 | describe('when configured as async', function () { 44 | 45 | beforeEach(function () { 46 | server = testRunner.http({respondImmediately: false}); 47 | }); 48 | 49 | afterEach(function () { 50 | server.stop(); 51 | }); 52 | 53 | beforeEach(function () { 54 | server.get('/greeting', function (req) { 55 | req.sendJson({ 56 | message: 'Hello from server!' 57 | }); 58 | }); 59 | }); 60 | 61 | it('does not resolve request unless respond()', function () { 62 | 63 | // given: 64 | var html = app.runHtml(''); 65 | 66 | // then: 67 | html.verify( 68 | expectElement('.greeting').toHaveText('initial') 69 | ); 70 | 71 | }); 72 | 73 | it('resolves request after respond()', function () { 74 | 75 | // given: 76 | var html = app.runHtml(''); 77 | 78 | // when: 79 | html.perform( 80 | server.respond 81 | ); 82 | 83 | // then: 84 | html.verify( 85 | expectElement('.greeting').toHaveText('Hello from server!') 86 | ); 87 | 88 | }); 89 | 90 | }); 91 | 92 | it('responds with specific code', function () { 93 | 94 | // given: 95 | server = testRunner.http(); 96 | server.get('/greeting', function (res) { 97 | res.sendStatus(418); 98 | }); 99 | 100 | // when: 101 | var html = app.runHtml(''); 102 | 103 | // then: 104 | html.verify( 105 | expectElement('.greeting').toHaveText('418') 106 | ); 107 | 108 | }); 109 | 110 | it('provides request body', function () { 111 | var requestedGreeting; 112 | // given: 113 | server = testRunner.http(); 114 | server.post(/\/greeting/, function (res) { 115 | requestedGreeting = res.body(); 116 | }); 117 | 118 | // when: 119 | var html = app.runHtml(''); 120 | html.perform( 121 | click.in('a') 122 | ); 123 | 124 | // then: 125 | expect(requestedGreeting).toEqual({who: 'John'}); 126 | }); 127 | 128 | it('provides request params', function () { 129 | var requestedParams; 130 | // given: 131 | server = testRunner.http(); 132 | server.post(/\/greeting/, function (res) { 133 | requestedParams = res.query(); 134 | }); 135 | 136 | // when: 137 | var html = app.runHtml(''); 138 | html.perform( 139 | click.in('a') 140 | ); 141 | 142 | // then: 143 | expect(requestedParams).toEqual({to: 'Jane Doe', when: 'now'}); 144 | }); 145 | 146 | it('provides request headers', function () { 147 | var myHeader; 148 | // given: 149 | server = testRunner.http(); 150 | server.post(/\/greeting/, function (req) { 151 | myHeader = req.header('x-my-header'); 152 | }); 153 | 154 | // when: 155 | var html = app.runHtml(''); 156 | html.perform( 157 | click.in('a') 158 | ); 159 | 160 | // then: 161 | expect(myHeader).toEqual('a-my-value'); 162 | }); 163 | 164 | }); 165 | 166 | -------------------------------------------------------------------------------- /test/internal/attach-to-dom-test.js: -------------------------------------------------------------------------------- 1 | describe('attach to dom test', function () { 2 | 3 | angular.module('attach-to-dom-app', []) 4 | .directive('greeting', function () { 5 | return { 6 | template: '
', 7 | scope: { 8 | name: '=' 9 | }, 10 | controllerAs: 'vm', 11 | controller: function () { 12 | this.extendInfoOnBody = function () { 13 | var body = angular.element(window.document.body); 14 | body.append('
Good morning!
') 15 | }; 16 | } 17 | }; 18 | }); 19 | 20 | var app; 21 | var click = testRunner.actions.click; 22 | var expectElement = testRunner.actions.expectElement; 23 | 24 | beforeEach(function () { 25 | app = testRunner.app(['attach-to-dom-app'], {attachToDocument: true}); 26 | }); 27 | 28 | afterEach(function () { 29 | app.stop(); 30 | }); 31 | 32 | it('message when adding element to dom directly in body 1', function () { 33 | var html = app.runHtml('', {defaultName: 'John'}); 34 | 35 | html.perform( 36 | click.in('#extendInfoOnBody') 37 | ); 38 | 39 | html.verify( 40 | expectElement('.message').toHaveLength(1) 41 | ); 42 | 43 | }); 44 | 45 | it('message when adding element to dom directly in body 2', function () { 46 | var html = app.runHtml('', {defaultName: 'John'}); 47 | 48 | html.perform( 49 | click.in('#extendInfoOnBody') 50 | ); 51 | 52 | html.verify( 53 | expectElement('.message').toHaveLength(1) 54 | ); 55 | 56 | }); 57 | 58 | 59 | }); 60 | -------------------------------------------------------------------------------- /test/sample-test.js: -------------------------------------------------------------------------------- 1 | describe('sample test', function () { 2 | 3 | angular.module('greeting-app', []) 4 | .directive('greeting', function () { 5 | return { 6 | template: ['
', 7 | '', 8 | '', 9 | '', 10 | '', 11 | '', 12 | '', 13 | '{{message}}', 14 | '{{tickled}}', 15 | '
', 16 | '
' 17 | ].join(''), 18 | scope: { 19 | name: '=' 20 | }, 21 | controllerAs: 'vm', 22 | controller: function ($http, $scope, $timeout) { 23 | this.sayHello = function () { 24 | $http.post('/greeting', {name: $scope.name}) 25 | .then(function (response) { 26 | var json = response.data; 27 | $scope.message = json.greeting; 28 | }); 29 | }; 30 | this.sayGoodbye = function () { 31 | $timeout(function () { 32 | $scope.message = 'Goodbye ' + $scope.name + '!'; 33 | }, 100); 34 | }; 35 | this.publish = function () { 36 | $scope.$emit('greeting', $scope.name); 37 | }; 38 | this.sayGoodMorning = function () { 39 | $scope.message = 'Good morning, ' + $scope.name + '!'; 40 | }; 41 | this.extendInfo = function () { 42 | var element = angular.element('.extended-info'); 43 | element.append('
Good evening!
') 44 | }; 45 | this.extendInfoOnBody = function () { 46 | var body = angular.element(window.document.body); 47 | body.append('
Good morning!
') 48 | }; 49 | $scope.$on('externalGreeting', function (event, greeting) { 50 | $scope.message = greeting; 51 | }) 52 | }, 53 | link: function (scope, element) { 54 | element.find('input').on('keydown', function (ev) { 55 | if (ev.which === 13) { 56 | scope.vm.sayHello(); 57 | } 58 | }); 59 | } 60 | }; 61 | }); 62 | 63 | var server, app; 64 | var type = testRunner.actions.type; 65 | var click = testRunner.actions.click; 66 | var expectElement = testRunner.actions.expectElement; 67 | var keydown = testRunner.actions.keydown; 68 | var wait = testRunner.actions.wait; 69 | var mouseover = testRunner.actions.mouseover; 70 | var mouseleave = testRunner.actions.mouseleave; 71 | var listenTo = testRunner.actions.listenTo; 72 | var publishEvent = testRunner.actions.publishEvent; 73 | var blur = testRunner.actions.blur; 74 | 75 | beforeEach(function () { 76 | 77 | app = testRunner.app(['greeting-app'], {attachToDocument: true}); 78 | server = testRunner.http(); 79 | }); 80 | 81 | afterEach(function () { 82 | server.stop(); 83 | app.stop(); 84 | }); 85 | 86 | beforeEach(function () { 87 | server.post('/greeting', function (req) { 88 | 89 | var body = req.body(); 90 | req.sendJson({ 91 | greeting: 'Hello ' + body.name + '!' 92 | }); 93 | }); 94 | }); 95 | 96 | it('populates name with default value', function () { 97 | 98 | // given: 99 | var html = app.runHtml('', {defaultName: 'John'}); 100 | 101 | // then: 102 | html.verify( 103 | expectElement('input.name').toHaveValue('John') 104 | ); 105 | 106 | }); 107 | 108 | it('greets person', function () { 109 | 110 | // given: 111 | var html = app.runHtml('', {defaultName: 'John'}); 112 | 113 | // when: 114 | html.perform( 115 | type('Jane').in('input.name'), 116 | click.in('button#hello') 117 | ); 118 | 119 | // then: 120 | html.verify( 121 | expectElement('.greeting').toContainText('Hello Jane!') 122 | ); 123 | 124 | }); 125 | 126 | it('greets person on enter', function () { 127 | // given: 128 | var html = app.runHtml('', {defaultName: 'John'}); 129 | 130 | // when: 131 | html.perform( 132 | type('Jane').in('input.name'), 133 | keydown(13).in('input.name') 134 | ); 135 | 136 | // then: 137 | html.verify( 138 | expectElement('.greeting').toContainText('Hello Jane!') 139 | ); 140 | 141 | }); 142 | 143 | it('says goodbye async', function (done) { 144 | 145 | // given: 146 | var html = app.runHtml('', {defaultName: 'John'}); 147 | 148 | // when: 149 | html.perform( 150 | click.in('button#goodbye') 151 | ); 152 | 153 | // then: 154 | html.verify( 155 | wait(200), 156 | expectElement('.greeting').toContainText('Goodbye John!'), 157 | done 158 | ); 159 | 160 | }); 161 | 162 | it('says goodbye async (fluent version)', function (done) { 163 | 164 | // given: 165 | var html = app.runHtml('', {defaultName: 'John'}); 166 | 167 | // when: 168 | html.perform( 169 | click.in('button#goodbye'), 170 | click.in('button#hello').after(200) 171 | ); 172 | 173 | // then: 174 | html.verify( 175 | expectElement('.greeting').toContainText('Hello John!'), 176 | done 177 | ); 178 | 179 | }); 180 | 181 | it('triggers mouseover and mouseleave', function () { 182 | 183 | var html = app.runHtml('', {}); 184 | 185 | html.perform(mouseover.in('#tickle-me')); 186 | html.verify(expectElement('#tickle-me').toContainText('true')); 187 | 188 | html.perform(mouseleave.in('#tickle-me')); 189 | html.verify(expectElement('#tickle-me').toContainText('false')); 190 | 191 | }); 192 | 193 | it('listens for events', function () { 194 | var greeted; 195 | // given: 196 | var html = app.runHtml('', {defaultName: 'John'}); 197 | 198 | // when: 199 | html.perform( 200 | listenTo('greeting', function (data) { 201 | greeted = data; 202 | }), 203 | click.in('#publisher') 204 | ); 205 | 206 | // then: 207 | expect(greeted).toEqual('John'); 208 | }); 209 | 210 | it('allows event publishing', function () { 211 | 212 | // given: 213 | var html = app.runHtml('', {}); 214 | 215 | // when: 216 | html.perform( 217 | publishEvent('externalGreeting', 'Hello, Jimmy!') 218 | ); 219 | 220 | // then: 221 | html.verify( 222 | expectElement('.greeting').toContainText('Hello, Jimmy!') 223 | ); 224 | }); 225 | 226 | it('message when losing focus on input', function () { 227 | // given: 228 | var html = app.runHtml('', {defaultName: 'John'}); 229 | 230 | // when: 231 | html.perform( 232 | type('Jane').in('input.name'), 233 | blur.from('input.name') 234 | ); 235 | 236 | // then: 237 | html.verify( 238 | expectElement('.greeting').toContainText('Good morning, Jane!') 239 | ); 240 | }); 241 | 242 | it('message when adding element to dom', function () { 243 | var html = app.runHtml('', {defaultName: 'John'}); 244 | 245 | html.perform( 246 | click.in('#extendInfo') 247 | ); 248 | 249 | html.verify( 250 | expectElement('.extended-info .message').toContainText('Good evening!') 251 | ); 252 | 253 | }); 254 | 255 | it('message when adding element to dom directly in body', function () { 256 | var html = app.runHtml('', {defaultName: 'John'}); 257 | 258 | html.perform( 259 | click.in('#extendInfoOnBody') 260 | ); 261 | 262 | html.verify( 263 | expectElement('.message').toContainText('Good morning!') 264 | ); 265 | 266 | }); 267 | 268 | it('last action in perform block was called', function (done) { 269 | // given: 270 | var html = app.runHtml('', {defaultName: 'John'}); 271 | 272 | // when: 273 | html.perform( 274 | click.in('button#goodbye'), 275 | wait(200) 276 | ); 277 | 278 | // then: 279 | html.verify( 280 | expectElement('.greeting').toContainText('Goodbye John!'), 281 | done 282 | ); 283 | }); 284 | 285 | }); 286 | --------------------------------------------------------------------------------