├── .codeclimate.yml ├── .eslintrc ├── .gitignore ├── .renovaterc ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── getter.d.ts ├── global.d.ts ├── index.d.ts ├── interface.d.ts ├── lib ├── define_var.js ├── interface.js ├── interface │ ├── dialects │ │ ├── bdd.js │ │ ├── bdd_getter_var.js │ │ └── bdd_global_var.js │ ├── index.js │ ├── jasmine.js │ ├── jest.js │ └── mocha.js ├── metadata.js ├── parse_message.js ├── suite_tracker.js ├── symbol.js └── variable.js ├── package-lock.json ├── package.json ├── spec ├── config.js ├── default_suite_tracking_examples.js ├── getter_defs_spec.js ├── global_defs_spec.js ├── interface_examples.js ├── interface_spec.js └── shared_behavior_spec.js └── tools ├── globals └── global.js ├── jasmine.js ├── jest.setup.js ├── karma.config.js └── rollup.umd.js /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | plugins: 3 | eslint: 4 | enabled: true 5 | checks: 6 | import/extensions: 7 | enabled: false 8 | exclude_patterns: 9 | - spec/**/* 10 | - tools/**/* 11 | - index.js 12 | - global.js 13 | - getter.js 14 | - "**/*.d.ts" 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "rules": { 4 | "no-plusplus": "off", 5 | "comma-dangle": "off", 6 | "no-underscore-dangle": ["error", { "allowAfterThis": true }], 7 | "no-param-reassign": ["error", { "props": false }], 8 | "no-mixed-operators": "off", 9 | "no-prototype-builtins": "off", 10 | "no-multi-assign": "off", 11 | "no-confusing-arrow": "off", 12 | "no-return-assign": "off", 13 | "arrow-body-style": "off", 14 | "no-use-before-define": ["error", { "functions": false }], 15 | "class-methods-use-this": "off", 16 | "prefer-destructuring": "off", 17 | "max-classes-per-file": "off" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | coverage.lcov 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | dist/ 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # Typescript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # dist 63 | index.js 64 | global.js 65 | getter.js 66 | /*.js.map 67 | -------------------------------------------------------------------------------- /.renovaterc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "pinVersions": false, 6 | "separatePatchReleases": false, 7 | "ignoreUnstable": true, 8 | "automerge": true, 9 | "automergeType": "branch-push", 10 | "lockFileMaintenance": { 11 | "enabled": true 12 | }, 13 | "optionalDependencies": { 14 | "versionStrategy": "widen" 15 | }, 16 | "packageRules": [ 17 | { 18 | "extends": "monorepo:babel6", 19 | "groupName": "babel6 monorepo" 20 | }, 21 | { 22 | "packageNames": [ 23 | "chai", 24 | "chai-spies", 25 | "mocha" 26 | ], 27 | "groupName": "mocha" 28 | }, 29 | { 30 | "packagePatterns": ["^karma"], 31 | "groupName": "karma" 32 | }, 33 | { 34 | "packagePatterns": ["^eslint"], 35 | "groupName": "eslint" 36 | }, 37 | { 38 | "packagePatterns": ["^rollup"], 39 | "groupName": "rollup" 40 | }, 41 | { 42 | "packagePatterns": ["jasmine"], 43 | "groupName": "jasmine" 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 10 # 2020-04-01 5 | - lts/* 6 | - node 7 | 8 | addons: 9 | chrome: stable 10 | 11 | services: 12 | - xvfb 13 | 14 | before_script: 15 | - npm run build 16 | - export DISPLAY=:99.0 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.6.1](https://github.com/stalniy/bdd-lazy-var/compare/v2.6.0...v2.6.1) (2021-03-14) 2 | 3 | # [2.6.0](https://github.com/stalniy/bdd-lazy-var/compare/v2.5.5...v2.6.0) (2020-12-10) 4 | 5 | 6 | ### Features 7 | 8 | * **sharedBehavior:** adds possibility to pass function in ([3e2196f](https://github.com/stalniy/bdd-lazy-var/commit/3e2196f546466771b98859b57894e2e472721d28)) 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to BDD Lazy Vars 2 | 3 | I would love for you to contribute to BDD Lazy Vars and help make it even better than it is 4 | today! As a contributor, here are the guidelines I would like you to follow: 5 | 6 | - [Question or Problem?](#question) 7 | - [Issues and Bugs](#issue) 8 | - [Feature Requests](#feature) 9 | - [Submission Guidelines](#submit) 10 | - [Coding Rules](#rules) 11 | - [Commit Message Guidelines](#commit) 12 | 13 | ## Got a Question or Problem? 14 | 15 | Do not open issues for general support questions as I want to keep GitHub issues for bug reports and feature requests. You've got much better chances of getting your question answered on [gitter chat][gitter]. 16 | 17 | ## Found a Bug? 18 | If you find a bug in the source code, you can help by [submitting an issue](#submit-issue) or even better, you can [submit a Pull Request](#submit-pr) with a fix. 19 | 20 | ## Missing a Feature? 21 | You can *request* a new feature by [submitting an issue](#submit-issue) to this GitHub Repository. If you would like to *implement* a new feature, please submit an issue with a proposal for your work first, to be sure that somebody else hasn't started to do the same. 22 | 23 | ## Submission Guidelines 24 | 25 | ### Submitting an Issue 26 | 27 | Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available. 28 | 29 | Please provide steps to reproduce for found bug (using http://plnkr.co or similar), this will help to understand and fix the issue faster. 30 | 31 | ### Submitting a Pull Request (PR) 32 | Before you submit your Pull Request (PR) consider the following guidelines: 33 | 34 | * Search [GitHub](https://github.com/stalniy/bdd-lazy-var/pulls) for an open or closed PR 35 | that relates to your submission. You don't want to duplicate effort. 36 | * Make your changes in a new git branch: 37 | 38 | ```sh 39 | git checkout -b my-fix-branch master 40 | ``` 41 | 42 | * **include appropriate test cases**. 43 | * Follow defined [Coding Rules](#rules). 44 | * Build distributable library files with `npm run build`. Run all test suites `npm test`. 45 | * Commit your changes using a descriptive commit message that follows defined [commit message conventions](#commit). Adherence to these conventions is necessary because release notes are automatically generated from these messages. 46 | * Push your branch to GitHub: 47 | 48 | ```shell 49 | git push origin my-fix-branch 50 | ``` 51 | * In GitHub, send a pull request to `bdd-lazy-var:master`. 52 | * If somebody from project contributors suggest changes then: 53 | * Make the required updates. 54 | * Re-run all test suites to ensure tests are still passing. 55 | * Rebase your branch and force push to your GitHub repository (this will update your Pull Request). Basically you can use `git commit -a --amend` and `git push --force origin my-fix-branch` in order to keep single commit in the feature branch. 56 | 57 | That's it! Thank you for your contribution! 58 | 59 | ## Coding Rules 60 | To ensure consistency throughout the source code, keep these rules in mind as you are working: 61 | 62 | * All features or bug fixes **must be tested** by one or more specs (unit-tests). 63 | * All public API methods **must be documented**. 64 | * Project follows [Airbnb's JavaScript Style Guide][js-style-guide] with [some exceptions](.eslintrc). All these will be checked by Travis ci when you submit your PR 65 | 66 | ## Commit Message Guidelines 67 | 68 | The project have very precise rules over how git commit messages can be formatted. This leads to **more readable messages** that are easy to follow when looking through the **project history**. But also, git history is used to **generate the change log**. 69 | 70 | The commit message format is borrowed from Angular projects and you can find [more details in this document][commit-message-format] 71 | 72 | [commit-message-format]: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit# 73 | [github]: https://github.com/stalniy/bdd-lazy-var 74 | [gitter]: https://gitter.im/bdd-lazy-var/Lobby 75 | [js-style-guide]: https://github.com/airbnb/javascript 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Sergiy Stotskiy 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BDD + lazy variable definition (aka rspec) 2 | 3 | [![BDD Lazy Var NPM version](https://badge.fury.io/js/bdd-lazy-var.svg)](http://badge.fury.io/js/bdd-lazy-var) 4 | [![Build Status](https://travis-ci.org/stalniy/bdd-lazy-var.svg?branch=master)](https://travis-ci.org/stalniy/bdd-lazy-var) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/65f79ae494101ba5f757/maintainability)](https://codeclimate.com/github/stalniy/bdd-lazy-var/maintainability) 6 | [![BDD Lazy Var Join the chat at https://gitter.im/bdd-lazy-var/Lobby](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/bdd-lazy-var/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 7 | 8 | Provides "ui" for testing frameworks such as [mocha][mocha], [jasmine][jasmine] and [jest][jest] which allows to define lazy variables and subjects. 9 | 10 | ## Purpose 11 | 12 | ### The old way 13 | 14 | ```js 15 | describe('Suite', function() { 16 | var name; 17 | 18 | beforeEach(function() { 19 | name = getName(); 20 | }); 21 | 22 | afterEach(function() { 23 | name = null; 24 | }); 25 | 26 | it('uses name variable', function() { 27 | expect(name).to.exist; 28 | }); 29 | 30 | it('does not use name but anyway it is created in beforeEach', function() { 31 | expect(1).to.equal(1); 32 | }); 33 | }); 34 | ``` 35 | 36 | ### Why should it be improved? 37 | 38 | Because as soon as amount of your tests increase, this pattern became increasingly difficult. 39 | Sometimes you will find yourself jumping around spec files, trying to find out where a given variable was initially defined. 40 | Or even worst, you may run into subtle bugs due to clobbering variables with common names (e.g. `model`, `view`) within a given scope, failing to realize they had already been defined. 41 | Furthermore, declaration statements in `describe` blocks will start looking something like: 42 | 43 | ```js 44 | var firstVar, secondVar, thirdVar, fourthVar, fifthVar, ..., nthVar 45 | ``` 46 | 47 | This is ugly and hard to parse. Finally, you can sometimes run into flaky tests due to "leaks" - test-specific variables that were not properly cleaned up after each case. 48 | 49 | ### The new, better way 50 | 51 | In an attempt to address these issues, I had with my e2e tests, I decided to create this library, which allows to define suite specific variables in more elegant way. 52 | So the original code above looks something like this: 53 | 54 | ```js 55 | describe('Suite', () => { 56 | def('name', () => `John Doe ${Math.random()}`); 57 | 58 | it('defines `name` variable', () => { 59 | expect($name).to.exist 60 | }); 61 | 62 | it('does not use name, so it is not created', () => { 63 | expect(1).to.equal(1); 64 | }); 65 | }); 66 | ``` 67 | 68 | ## Why the new way rocks 69 | 70 | Switching over to this pattern has yielded a significant amount of benefits for us, including: 71 | 72 | ### No more global leaks 73 | 74 | Because lazy vars are cleared after each test, we didn't have to worry about test pollution anymore. This helped ensure isolation between our tests, making them a lot more reliable. 75 | 76 | ### Clear meaning 77 | 78 | Every time I see a `$` reference in my tests, I know where it's defined. 79 | That, coupled with removing exhaustive `var` declarations in `describe` blocks, have made even my largest tests clear and understandable. 80 | 81 | ### Lazy evaluation 82 | 83 | Variables are instantiated only when referenced. 84 | That means if you don't use variable inside your test it won't be evaluated, making your tests to run faster. 85 | No useless instantiation any more! 86 | 87 | ### Composition 88 | 89 | Due to laziness we are able to compose variables. This allows to define more general varibles at the top level and more specific at the bottom: 90 | 91 | ```js 92 | describe('User', function() { 93 | subject('user', () => new User($props)) 94 | 95 | describe('when user is "admin"', function() { 96 | def('props', () => ({ role: 'admin' })) 97 | 98 | it('can update articles', function() { 99 | // user is created with property role equal "admin" 100 | expect($user).to.... 101 | }) 102 | }) 103 | 104 | describe('when user is "member"', function() { 105 | def('props', () => ({ role: 'member' })) 106 | 107 | it('cannot update articles', function() { 108 | // user is created with property role equal "member" 109 | expect($user).to.... 110 | }) 111 | }) 112 | }) 113 | ``` 114 | 115 | ## Tests reusage 116 | 117 | Very often you may find that some behavior repeats (e.g., when you implement Adapter pattern), 118 | and you would like to reuse tests for a different class or object. 119 | To do this [Wiki of Mocha.js](https://github.com/mochajs/mocha/wiki/Shared-Behaviours) recommend to move your tests into separate function and call it whenever you need it. 120 | 121 | I prefer to be more explicit in doing this, that's why created few helper methods: 122 | * `sharedExamplesFor` - defines a set of reusable tests. When you call this function, it just stores your tests 123 | * `includeExamplesFor` - runs previously defined examples in current context (i.e., in current `describe`) 124 | * `itBehavesLike` - runs defined examples in nested context (i.e., in nested `describe`) 125 | 126 | `sharedExamplesFor` defines shared examples in the scope of the currently defining suite. 127 | If you call this function outside `describe` (or `context`) it defines shared examples globally. 128 | 129 | **WARNING**: files containing shared examples must be loaded before the files that 130 | use them. 131 | 132 | #### Scenarios 133 | 134 |
135 | shared examples group included in two groups in one file 136 | 137 | ```js 138 | sharedExamplesFor('a collection', () => { 139 | it('has three items', () => { 140 | expect($subject.size).to.equal(3) 141 | }) 142 | 143 | describe('#has', () => { 144 | it('returns true with an item that is in the collection', () => { 145 | expect($subject.has(7)).to.be.true 146 | }) 147 | 148 | it('returns false with an item that is not in the collection', () => { 149 | expect($subject.has(9)).to.be.false 150 | }) 151 | }) 152 | }) 153 | 154 | describe('Set', () => { 155 | subject(() => new Set([1, 2, 7])) 156 | 157 | itBehavesLike('a collection') 158 | }) 159 | 160 | describe('Map', () => { 161 | subject(() => new Map([[2, 1], [7, 5], [3, 4]])) 162 | 163 | itBehavesLike('a collection') 164 | }) 165 | ``` 166 |
167 | 168 |
169 | Passing parameters to a shared example group 170 | 171 | ```js 172 | sharedExamplesFor('a collection', (size, existingItem, nonExistingItem) => { 173 | it('has three items', () => { 174 | expect($subject.size).to.equal(size) 175 | }) 176 | 177 | describe('#has', () => { 178 | it('returns true with an item that is in the collection', () => { 179 | expect($subject.has(existingItem)).to.be.true 180 | }) 181 | 182 | it('returns false with an item that is not in the collection', () => { 183 | expect($subject.has(nonExistingItem)).to.be.false 184 | }) 185 | }) 186 | }) 187 | 188 | describe('Set', () => { 189 | subject(() => new Set([1, 2, 7])) 190 | 191 | itBehavesLike('a collection', 3, 2, 10) 192 | }) 193 | 194 | describe('Map', () => { 195 | subject(() => new Map([[2, 1]])) 196 | 197 | itBehavesLike('a collection', 1, 2, 3) 198 | }) 199 | ``` 200 |
201 | 202 |
203 | Passing lazy vars to a shared example group 204 | 205 | There are 2 ways how to pass lazy variables: 206 | * all variables are inherited by nested contexts (i.e., `describe` calls), 207 | so you can rely on variable name, as it was done with `subject` in previous examples 208 | * you can pass variable definition using `get.variable` helper 209 | 210 | ```js 211 | sharedExamplesFor('a collection', (collection) => { 212 | def('collection', collection) 213 | 214 | it('has three items', () => { 215 | expect($collection.size).to.equal(1) 216 | }) 217 | 218 | describe('#has', () => { 219 | it('returns true with an item that is in the collection', () => { 220 | expect($collection.has(7)).to.be.true 221 | }) 222 | 223 | it('returns false with an item that is not in the collection', () => { 224 | expect($collection.has(9)).to.be.false 225 | }) 226 | }) 227 | }) 228 | 229 | describe('Set', () => { 230 | subject(() => new Set([7])) 231 | 232 | itBehavesLike('a collection', get.variable('subject')) 233 | }) 234 | 235 | describe('Map', () => { 236 | subject(() => new Map([[2, 1]])) 237 | 238 | itBehavesLike('a collection', get.variable('subject')) 239 | }) 240 | ``` 241 |
242 | 243 | ## Shortcuts 244 | 245 | Very often we want to declare several test cases which tests subject's field or subject's behavior. 246 | To do this quickly you can use `its` or `it` without message: 247 | 248 |
249 | Shortcuts example 250 | 251 | ```js 252 | describe('Array', () => { 253 | subject(() => ({ 254 | items: [1, 2, 3], 255 | name: 'John' 256 | })) 257 | 258 | its('items.length', () => is.expected.to.equal(3)) // i.e. expect($subject.items.length).to.equal(3) 259 | its('name', () => is.expected.to.equal('John')) // i.e. expect($subject.name).to.equal('John') 260 | 261 | // i.e. expect($subject).to.have.property('items').which.has.length(3) 262 | it(() => is.expected.to.have.property('items').which.has.length(3)) 263 | }) 264 | ``` 265 | 266 | Also it generates messages for you based on passed in function body. The example above reports: 267 | 268 | ```sh 269 | Array 270 | ✓ is expected to have property('items') which has length(3) 271 | items.length 272 | ✓ is expected to equal(3) 273 | name 274 | ✓ is expected to equal('John') 275 | ``` 276 |
277 | 278 | **Note**: if you use `mocha` and `chai` make sure that defines `global.expect = chai.expect`, otherwise `is.expected` will throw error that `context.expect` is `undefined`. 279 | 280 | ## Installation 281 | 282 | ```bash 283 | npm install bdd-lazy-var --save-dev 284 | ``` 285 | 286 |
287 | Mocha.js 288 | 289 | #### Command line 290 | ```sh 291 | mocha -u bdd-lazy-var/global 292 | ``` 293 | 294 | #### In JavaScript 295 | 296 | See [Using Mocha programatically](https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically) 297 | 298 | ```js 299 | const Mocha = require('mocha'); 300 | 301 | const mocha = new Mocha({ 302 | ui: 'bdd-lazy-var/global' // bdd-lazy-var or bdd-lazy-var/getter 303 | }); 304 | 305 | mocha.addFile(...) 306 | mocha.run(...) 307 | 308 | // !!! Important the next code should be written in a separate file 309 | // later you can either use `get` and `def` as global functions 310 | // or export them from corresponding module 311 | const { get, def } = require('bdd-lazy-var/global'); 312 | 313 | describe('Test', () => { 314 | // ... 315 | }) 316 | ``` 317 | 318 | #### Using karma (via karma-mocha npm package) 319 | 320 | **Note** requires `karma-mocha` `^1.1.1` 321 | 322 | So, in `karma.config.js` it looks like this: 323 | ```js 324 | module.exports = function(config) { 325 | config.set({ 326 | // .... 327 | client: { 328 | mocha: { 329 | ui: 'bdd-lazy-var/global', 330 | require: [require.resolve('bdd-lazy-var/global')] 331 | } 332 | } 333 | }); 334 | } 335 | ``` 336 |
337 | 338 |
339 | Jasmine.js 340 | 341 | #### Command line 342 | 343 | ```sh 344 | jasmine --helper=node_modules/bdd-lazy-var/global.js 345 | ``` 346 | 347 | or using `spec/spec_helper.js` 348 | 349 | ```js 350 | require('bdd-lazy-var/global'); 351 | 352 | // ... other helper stuff 353 | ``` 354 | 355 | and then 356 | 357 | ```sh 358 | jasmine --helper=spec/*_helper.js 359 | ``` 360 | 361 | #### In JavaScript 362 | 363 | When you want programatically run jasmine 364 | 365 | ```js 366 | require('jasmine-core'); 367 | 368 | // !!! Important the next code should be written in a separate file 369 | // later you can either use `get` and `def` as global functions 370 | // or export them from corresponding module 371 | const { get, def } = require('bdd-lazy-var/global'); 372 | 373 | describe('Test', () => { 374 | // ... 375 | }) 376 | ``` 377 | 378 | #### Using karma (via karma-jasmine npm package) 379 | 380 | So, in `karma.config.js` it looks like this: 381 | 382 | ```js 383 | module.exports = function(config) { 384 | config.set({ 385 | // .... 386 | files: [ 387 | 'node_modules/bdd-lazy-var/global.js', 388 | // ... your specs here 389 | ] 390 | }); 391 | } 392 | ``` 393 |
394 | 395 |
396 | Jest 397 | 398 | #### Command line 399 | 400 | Use Jest as usually if you export `get` and `def` from corresponding module 401 | 402 | ```js 403 | jest 404 | ``` 405 | 406 | In case you want to use global `get` and `def` 407 | 408 | ```sh 409 | jest --setupTestFrameworkScriptFile bdd-lazy-var/global 410 | ``` 411 | 412 | #### In JavaScript 413 | 414 | ```js 415 | // later you can either use `get` and `def` as global functions 416 | // or export them from relative module 417 | const { get, def } = require('bdd-lazy-var/global'); 418 | ``` 419 |
420 | 421 | ## Dialects 422 | 423 | `bdd-lazy-var` provides 3 different dialects: 424 | * access variables by referencing `$` (the recommended one, available by requiring `bdd-lazy-var/global`) 425 | * access variables by referencing `get.` (more strict, available by requiring `bdd-lazy-var/getter`) 426 | * access variables by referencing `get('')` (the most strict and less readable way, available by requiring `bdd-lazy-var`) 427 | 428 | All are bundled as UMD versions. Each dialect is compiled in a separate file and should be required or provided for testing framework. 429 | 430 | ### Aliases 431 | 432 | In accordance with Rspec's DDL, `context`, `xcontext`, and `fcontext` have been aliased to their related `describe` commands for both the Jest and Jasmine testing libraries. Mocha's BDD interface already provides this keyword. 433 | 434 | ## The Core Features 435 | * lazy instantiation, allows variable composition 436 | * automatically cleaned after each test 437 | * accessible inside `before/beforeAll`, `after/afterAll` callbacks 438 | * named `subject`s to be more explicit 439 | * ability to shadow parent's variable 440 | * variable inheritance with access to parent variables 441 | * supports typescript 442 | 443 | For more information, read [the article on Medium](https://medium.com/@sergiy.stotskiy/lazy-variables-with-mocha-js-d6063503104c#.ceo9jvrzh). 444 | 445 | ## TypeScript Notes 446 | 447 | It's also possible to use `bdd-lazy-var` with TypeScript. The best integrated dialects are `get` and `getter`. To do so, you need either include corresponding definitions in your [tsconfig.json](http://www.typescriptlang.org/docs/handbook/tsconfig-json.html) or use ES6 module system. 448 | 449 |
450 | tsconfig.json 451 | 452 | ```js 453 | { 454 | "compilerOptions": { 455 | "module": "commonjs", 456 | "removeComments": true, 457 | "preserveConstEnums": true, 458 | "sourceMap": true 459 | }, 460 | "include": [ 461 | "src/**/*", 462 | "node_modules/bdd-lazy-var/index.d.ts" // for `get('')` syntax 463 | // or 464 | "node_modules/bdd-lazy-var/getter.d.ts" // for `get.` syntax 465 | ] 466 | } 467 | ``` 468 |
469 | 470 |
471 | ES6 module system 472 | 473 | ```js 474 | import { get, def } from 'bdd-lazy-var' 475 | // or 476 | import { get, def } from 'bdd-lazy-var/getter' 477 | 478 | describe('My Test', () => { 479 | // .... 480 | }) 481 | ``` 482 | 483 | In this case TypeScript loads corresponding declarations automatically 484 |
485 | 486 | It's a bit harder to work with `global` dialect. It creates global getters on the fly, so there is no way to let TypeScript know something about these variables, thus you need to `declare` them manually. 487 | 488 |
489 | TypeScript and global dialect 490 | 491 | ```ts 492 | import { def } from 'bdd-lazy-var/global' 493 | 494 | describe('My Test', () => { 495 | declare let $value: number // <-- need to place this declarations manually 496 | def('value', () => 5) 497 | 498 | it('equals 5', () => { 499 | expect($value).to.equal(5) 500 | }) 501 | }) 502 | ``` 503 | 504 | As with other dialects you can either use `import` statements to load typings automatically or add them manually in `tsconfig.json` 505 |
506 | 507 | ## Examples 508 | 509 |
510 | Test with subject 511 | 512 | ```js 513 | describe('Array', () => { 514 | subject(() => [1, 2, 3]); 515 | 516 | it('has 3 elements by default', () => { 517 | expect($subject).to.have.length(3); 518 | }); 519 | }); 520 | ``` 521 |
522 | 523 |
524 | Named subject 525 | 526 | ```js 527 | describe('Array', () => { 528 | subject('collection', () => [1, 2, 3]); 529 | 530 | it('has 3 elements by default', () => { 531 | expect($subject).to.equal($collection); 532 | expect($collection).to.have.length(3); 533 | }); 534 | }); 535 | ``` 536 |
537 | 538 |
539 | `beforeEach` and redefined subject 540 | 541 | ```js 542 | describe('Array', () => { 543 | subject('collection', () => [1, 2, 3]); 544 | 545 | beforeEach(() => { 546 | // this beforeEach is executed for tests of suite with subject equal [1, 2, 3] 547 | // and for nested describe with subject being [] 548 | $subject.push(4); 549 | }); 550 | 551 | it('has 3 elements by default', () => { 552 | expect($subject).to.equal($collection); 553 | expect($collection).to.have.length(3); 554 | }); 555 | 556 | describe('when empty', () => { 557 | subject(() => []); 558 | 559 | it('has 1 element', () => { 560 | expect($subject).not.to.equal($collection); 561 | expect($collection).to.deep.equal([4]); 562 | }); 563 | }); 564 | }); 565 | ``` 566 |
567 | 568 |
569 | Access parent variable in child variable definition 570 | 571 | ```js 572 | describe('Array', () => { 573 | subject('collection', () => [1, 2, 3]); 574 | 575 | it('has 3 elements by default', () => { 576 | expect($subject).to.equal($collection); 577 | expect($collection).to.have.length(3); 578 | }); 579 | 580 | describe('when empty', () => { 581 | subject(() => { 582 | // in this definition `$subject` references parent $subject (i.e., `$collection` variable) 583 | return $subject.concat([4, 5]); 584 | }); 585 | 586 | it('is properly uses parent subject', () => { 587 | expect($subject).not.to.equal($collection); 588 | expect($collection).to.deep.equal([1, 2, 3, 4, 5]); 589 | }); 590 | }); 591 | }); 592 | ``` 593 |
594 | 595 | ## Want to help? 596 | 597 | Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on guidelines for [contributing][contributing] 598 | 599 | ## License 600 | 601 | [MIT License](http://www.opensource.org/licenses/MIT) 602 | 603 | [mocha]: https://mochajs.org 604 | [jasmine]: https://jasmine.github.io/2.0/introduction.html 605 | [jest]: https://facebook.github.io/jest/docs/en/getting-started.html 606 | [contributing]: https://github.com/stalniy/bdd-lazy-var/blob/master/CONTRIBUTING.md 607 | -------------------------------------------------------------------------------- /getter.d.ts: -------------------------------------------------------------------------------- 1 | declare module './interface' { 2 | interface GetLazyVar { 3 | (name: string): any; 4 | [name: string]: any; 5 | } 6 | } 7 | 8 | export * from './index' 9 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | export * from './index' 2 | 3 | // TODO: declare `$` globally 4 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import * as lv from './interface' 2 | export * from './interface' 3 | 4 | declare global { 5 | const get: typeof lv.get; 6 | const def: typeof lv.def; 7 | const sharedExamplesFor: typeof lv.sharedExamplesFor; 8 | const itBehavesLike: typeof lv.itBehavesLike; 9 | const includeExamplesFor: typeof lv.includeExamplesFor; 10 | } 11 | -------------------------------------------------------------------------------- /interface.d.ts: -------------------------------------------------------------------------------- 1 | interface GetLazyVar { 2 | (name: string): any; 3 | } 4 | 5 | type AnyFunction = (...args: any[]) => any; 6 | 7 | export const get: GetLazyVar; 8 | export function def(name: string, implementation: () => any): void; 9 | export function sharedExamplesFor(summary: string, implementation: (...vars: any[]) => void): void; 10 | export function itBehavesLike(summary: string | AnyFunction, ...vars: any[]): void; 11 | export function includeExamplesFor(summary: string | AnyFunction, ...vars: any[]): void; 12 | -------------------------------------------------------------------------------- /lib/define_var.js: -------------------------------------------------------------------------------- 1 | const prop = require('./symbol').for; 2 | 3 | const LAZY_VARS_PROP_NAME = prop('__lazyVars'); 4 | 5 | function defineGetter(context, varName, options) { 6 | const params = { 7 | getterPrefix: '', 8 | defineOn: context, 9 | ...options 10 | }; 11 | 12 | const accessorName = params.getterPrefix + varName; 13 | const varContext = params.defineOn; 14 | const vars = varContext[LAZY_VARS_PROP_NAME] = varContext[LAZY_VARS_PROP_NAME] || {}; 15 | 16 | if (accessorName in vars) { 17 | return; 18 | } 19 | 20 | if (accessorName in varContext) { 21 | throw new Error(`Cannot create lazy variable "${varName}" as variable with the same name exists on the provided context`); 22 | } 23 | 24 | vars[accessorName] = true; 25 | Object.defineProperty(varContext, accessorName, { 26 | configurable: true, 27 | get: () => context.get(varName) 28 | }); 29 | } 30 | 31 | module.exports = defineGetter; 32 | -------------------------------------------------------------------------------- /lib/interface.js: -------------------------------------------------------------------------------- 1 | const { Metadata } = require('./metadata'); 2 | const Variable = require('./variable'); 3 | const { parseMessage, humanize } = require('./parse_message'); 4 | 5 | module.exports = (context, tracker, options) => { 6 | const get = (varName) => Variable.evaluate(varName, { in: tracker.currentContext }); 7 | 8 | get.definitionOf = get.variable = (varName) => get.bind(null, varName); 9 | 10 | function def(varName, definition) { 11 | const suite = tracker.currentlyDefinedSuite; 12 | 13 | if (!Array.isArray(varName)) { 14 | Metadata.ensureDefinedOn(suite).addVar(varName, definition); 15 | runHook('onDefineVariable', suite, varName); 16 | return; 17 | } 18 | 19 | const [name, ...aliases] = varName; 20 | def(name, definition); 21 | 22 | const metadata = Metadata.of(suite); 23 | aliases.forEach((alias) => { 24 | metadata.addAliasFor(name, alias); 25 | runHook('onDefineVariable', suite, alias); 26 | }); 27 | } 28 | 29 | function subject(...args) { 30 | const [name, definition] = args; 31 | 32 | if (args.length === 1) { 33 | return def('subject', name); 34 | } 35 | 36 | if (args.length === 2) { 37 | return def([name, 'subject'], definition); 38 | } 39 | 40 | return get('subject'); 41 | } 42 | 43 | function sharedExamplesFor(name, defs) { 44 | Metadata.ensureDefinedOn(tracker.currentlyDefinedSuite) 45 | .addExamplesFor(name, defs); 46 | } 47 | 48 | function includeExamplesFor(nameOrFn, ...args) { 49 | const meta = Metadata.ensureDefinedOn(tracker.currentlyDefinedSuite); 50 | 51 | if (typeof nameOrFn === 'function') { 52 | nameOrFn(...args); 53 | } else { 54 | meta.runExamplesFor(nameOrFn, args); 55 | } 56 | } 57 | 58 | function itBehavesLike(...args) { 59 | const nameOrFn = args[0]; 60 | const title = typeof nameOrFn === 'function' ? humanize(nameOrFn.name || 'this') : nameOrFn; 61 | 62 | context.describe(`behaves like ${title}`, () => { 63 | includeExamplesFor(...args); 64 | }); 65 | } 66 | 67 | const wrapIts = (test) => function its(prop, messageOrAssert, fn) { 68 | const [message, assert] = typeof messageOrAssert === 'function' 69 | ? [parseMessage(messageOrAssert), messageOrAssert] 70 | : [messageOrAssert, fn]; 71 | 72 | return context.describe(prop, () => { 73 | def('__itsSubject__', () => prop.split('.').reduce((object, field) => { 74 | const value = object[field]; 75 | 76 | return typeof value === 'function' 77 | ? object[field]() 78 | : value; 79 | }, subject())); 80 | 81 | test(message || 'is correct', assert); 82 | }); 83 | }; 84 | 85 | // TODO: `shouldWrapAssert` can be removed when https://github.com/facebook/jest/issues/6516 fixed 86 | const wrapIt = (test, shouldWrapAssert) => function it(...args) { 87 | if (typeof args[0] === 'function') { 88 | args.unshift(parseMessage(args[0])); 89 | } 90 | 91 | if (shouldWrapAssert) { 92 | const assert = args[1]; 93 | args[1] = function testWrapper(...testArgs) { 94 | const value = assert.apply(this, testArgs); 95 | return value && typeof value.then === 'function' ? value : undefined; 96 | }; 97 | } 98 | 99 | return test(...args); 100 | }; 101 | 102 | function runHook(name, suite, varName) { 103 | if (name && typeof options[name] === 'function') { 104 | options[name](suite, varName, context); 105 | } 106 | } 107 | 108 | const is = { 109 | get expected() { 110 | const name = Metadata.of(tracker.currentContext, '__itsSubject__') 111 | ? '__itsSubject__' 112 | : 'subject'; 113 | return context.expect(get(name)); 114 | } 115 | }; 116 | 117 | return { 118 | subject, 119 | def, 120 | get, 121 | wrapIt, 122 | wrapIts, 123 | is, 124 | sharedExamplesFor, 125 | includeExamplesFor, 126 | itBehavesLike 127 | }; 128 | }; 129 | -------------------------------------------------------------------------------- /lib/interface/dialects/bdd.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../index').createUi('bdd-lazy-var'); 2 | -------------------------------------------------------------------------------- /lib/interface/dialects/bdd_getter_var.js: -------------------------------------------------------------------------------- 1 | const interfaceBuilder = require('../index'); 2 | const defineGetterOnce = require('../../define_var'); 3 | 4 | module.exports = interfaceBuilder.createUi('bdd-lazy-var/getter', { 5 | onDefineVariable(suite, varName, context) { 6 | defineGetterOnce(context, varName, { defineOn: context.get }); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /lib/interface/dialects/bdd_global_var.js: -------------------------------------------------------------------------------- 1 | const interfaceBuilder = require('../index'); 2 | const defineGetterOnce = require('../../define_var'); 3 | 4 | module.exports = interfaceBuilder.createUi('bdd-lazy-var/global', { 5 | onDefineVariable(suite, varName, context) { 6 | defineGetterOnce(context, varName, { getterPrefix: '$' }); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /lib/interface/index.js: -------------------------------------------------------------------------------- 1 | let Mocha; 2 | 3 | try { 4 | Mocha = require('mocha'); // eslint-disable-line 5 | } catch (e) { 6 | // eslint-disable-line 7 | } 8 | 9 | let ui; 10 | 11 | if (typeof jest !== 'undefined') { 12 | ui = require('./jest'); // eslint-disable-line 13 | } else if (global.jasmine) { 14 | ui = require('./jasmine'); // eslint-disable-line 15 | } else if (Mocha) { 16 | ui = require('./mocha'); // eslint-disable-line 17 | } 18 | 19 | if (!ui) { 20 | throw new Error(` 21 | Unable to detect testing framework. Make sure that 22 | * jest, jasmine or mocha is installed 23 | * bdd-lazy-var is included after "jasmine" or "mocha" 24 | `); 25 | } 26 | 27 | module.exports = ui; 28 | -------------------------------------------------------------------------------- /lib/interface/jasmine.js: -------------------------------------------------------------------------------- 1 | const createLazyVarInterface = require('../interface'); 2 | const SuiteTracker = require('../suite_tracker'); 3 | 4 | function createSuiteTracker() { 5 | return { 6 | before(tracker, suite) { 7 | global.beforeAll(tracker.registerSuite.bind(tracker, suite)); 8 | global.afterAll(tracker.cleanUpCurrentAndRestorePrevContext); 9 | }, 10 | 11 | after(tracker) { 12 | global.beforeAll(tracker.cleanUpCurrentContext); 13 | } 14 | }; 15 | } 16 | 17 | function addInterface(rootSuite, options) { 18 | const context = global; 19 | const tracker = new options.Tracker({ rootSuite, suiteTracker: createSuiteTracker() }); 20 | const { wrapIts, wrapIt, ...ui } = createLazyVarInterface(context, tracker, options); 21 | const isJest = typeof jest !== 'undefined'; 22 | 23 | Object.assign(context, ui); 24 | ['', 'x', 'f'].forEach((prefix) => { 25 | const describeKey = `${prefix}describe`; 26 | const itKey = `${prefix}it`; 27 | 28 | context[`${itKey}s`] = wrapIts(context[itKey]); 29 | context[itKey] = wrapIt(context[itKey], isJest); 30 | context[describeKey] = tracker.wrapSuite(context[describeKey]); 31 | context[`${prefix}context`] = context[describeKey]; 32 | }); 33 | context.afterEach(tracker.cleanUpCurrentContext); 34 | 35 | return ui; 36 | } 37 | 38 | module.exports = { 39 | createUi(name, options) { 40 | const config = { Tracker: SuiteTracker, ...options }; 41 | 42 | return addInterface(global.jasmine.getEnv().topSuite(), config); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /lib/interface/jest.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./jasmine'); 2 | -------------------------------------------------------------------------------- /lib/interface/mocha.js: -------------------------------------------------------------------------------- 1 | const Mocha = require('mocha'); // eslint-disable-line 2 | const createLazyVarInterface = require('../interface'); 3 | const SuiteTracker = require('../suite_tracker'); 4 | 5 | function createSuiteTracker() { 6 | return { 7 | before(tracker, suite) { 8 | suite.beforeAll(tracker.registerSuite.bind(tracker, suite)); 9 | }, 10 | 11 | after(tracker, suite) { 12 | suite.beforeAll(tracker.cleanUpCurrentContext); 13 | suite.afterAll(tracker.cleanUpCurrentAndRestorePrevContext); 14 | } 15 | }; 16 | } 17 | 18 | function addInterface(rootSuite, options) { 19 | const tracker = new options.Tracker({ rootSuite, suiteTracker: createSuiteTracker() }); 20 | let ui; 21 | 22 | rootSuite.afterEach(tracker.cleanUpCurrentContext); 23 | rootSuite.on('pre-require', (context) => { 24 | const { describe, it } = context; 25 | 26 | if (!ui) { 27 | ui = createLazyVarInterface(context, tracker, options); 28 | const { wrapIts, wrapIt, ...restUi } = ui; 29 | Object.assign(context, restUi); 30 | } 31 | 32 | context.its = ui.wrapIts(it); 33 | context.its.only = ui.wrapIts(it.only); 34 | context.its.skip = ui.wrapIts(it.skip); 35 | context.it = ui.wrapIt(it); 36 | context.it.only = ui.wrapIt(it.only); 37 | context.it.skip = ui.wrapIt(it.skip); 38 | context.describe = tracker.wrapSuite(describe); 39 | context.describe.skip = tracker.wrapSuite(describe.skip); 40 | context.describe.only = tracker.wrapSuite(describe.only); 41 | context.context = context.describe; 42 | context.xdescribe = context.xcontext = context.describe.skip; 43 | }); 44 | } 45 | 46 | module.exports = { 47 | createUi(name, options) { 48 | const config = { 49 | Tracker: SuiteTracker, 50 | inheritUi: 'bdd', 51 | ...options 52 | }; 53 | 54 | Mocha.interfaces[name] = (rootSuite) => { 55 | Mocha.interfaces[config.inheritUi](rootSuite); 56 | return addInterface(rootSuite, config); 57 | }; 58 | 59 | const getters = ['get', 'def', 'subject', 'its', 'it', 'is', 'sharedExamplesFor', 'includeExamplesFor', 'itBehavesLike']; 60 | const defs = getters.reduce((all, uiName) => { 61 | all[uiName] = { get: () => global[uiName] }; 62 | return all; 63 | }, {}); 64 | 65 | return Object.defineProperties(Mocha.interfaces[name], defs); 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /lib/metadata.js: -------------------------------------------------------------------------------- 1 | const Symbol = require('./symbol'); 2 | 3 | const LAZY_VARS_FIELD = Symbol.for('__lazyVars'); 4 | const EXAMPLES_PREFIX = '__SH_EX:'; 5 | 6 | class VariableMetadata { 7 | constructor(name, definition, metadata) { 8 | this.value = definition; 9 | this.parent = metadata; 10 | this.names = { [name]: true }; 11 | } 12 | 13 | addName(name) { 14 | this.names[name] = true; 15 | return this; 16 | } 17 | 18 | isNamedAs(name) { 19 | return this.names[name]; 20 | } 21 | 22 | evaluate() { 23 | return typeof this.value === 'function' 24 | ? this.value() 25 | : this.value; 26 | } 27 | } 28 | 29 | class Metadata { 30 | static of(context, varName) { 31 | const metadata = context[LAZY_VARS_FIELD]; 32 | 33 | return varName && metadata ? metadata.defs[varName] : metadata; 34 | } 35 | 36 | static ensureDefinedOn(context) { 37 | if (!context.hasOwnProperty(LAZY_VARS_FIELD)) { 38 | context[LAZY_VARS_FIELD] = new Metadata(); 39 | } 40 | 41 | return context[LAZY_VARS_FIELD]; 42 | } 43 | 44 | constructor() { 45 | this.defs = {}; 46 | this.values = {}; 47 | this.hasValues = false; 48 | this.defined = false; 49 | } 50 | 51 | getVar(name) { 52 | if (!this.values.hasOwnProperty(name) && this.defs[name]) { 53 | this.hasValues = true; 54 | this.values[name] = this.evaluate(name); 55 | } 56 | 57 | return this.values[name]; 58 | } 59 | 60 | evaluate(name) { 61 | return this.defs[name].evaluate(); 62 | } 63 | 64 | addChild(child) { 65 | child.defs = Object.assign(Object.create(this.defs), child.defs); 66 | child.parent = this.defined ? this : this.parent; 67 | } 68 | 69 | addVar(name, definition) { 70 | if (this.defs.hasOwnProperty(name)) { 71 | throw new Error(`Cannot define "${name}" variable twice in the same suite.`); 72 | } 73 | 74 | this.defined = true; 75 | this.defs[name] = new VariableMetadata(name, definition, this); 76 | 77 | return this; 78 | } 79 | 80 | addAliasFor(name, aliasName) { 81 | this.defs[aliasName] = this.defs[name].addName(aliasName); 82 | } 83 | 84 | releaseVars() { 85 | if (this.hasValues) { 86 | this.values = {}; 87 | this.hasValues = false; 88 | } 89 | } 90 | 91 | lookupMetadataFor(varName) { 92 | const varMeta = this.defs[varName]; 93 | const definedIn = varMeta.parent; 94 | 95 | if (!varMeta || !definedIn.parent.defs[varName]) { 96 | throw new Error(`Unknown parent variable "${varName}".`); 97 | } 98 | 99 | return definedIn.parent; 100 | } 101 | 102 | addExamplesFor(name, definition) { 103 | const examplesName = EXAMPLES_PREFIX + name; 104 | 105 | if (this.defs.hasOwnProperty(examplesName)) { 106 | throw new Error(`Attempt to override "${name}" shared example`); 107 | } 108 | 109 | return this.addVar(examplesName, definition); 110 | } 111 | 112 | runExamplesFor(name, args) { 113 | const examples = this.defs[EXAMPLES_PREFIX + name]; 114 | 115 | if (!examples) { 116 | throw new Error(`Attempt to include not defined shared behavior "${name}"`); 117 | } 118 | 119 | return examples.value(...args); 120 | } 121 | } 122 | 123 | module.exports = { Metadata }; 124 | -------------------------------------------------------------------------------- /lib/parse_message.js: -------------------------------------------------------------------------------- 1 | function parseMessage(fn) { 2 | const matches = fn.toString().match(/is\.expected\.(\s+(?=\.)|.)+/g); 3 | 4 | if (!matches) { 5 | return ''; 6 | } 7 | 8 | const prefixLength = 'is.expected.'.length; 9 | const body = matches.reduce((message, chunk) => { 10 | const cleanChunk = chunk.trim() 11 | .slice(prefixLength) 12 | .replace(/[\s.]+/g, ' '); 13 | const humanized = humanize(cleanChunk).replace(/ and /g, ', '); 14 | message.push(humanized); 15 | return message; 16 | }, []); 17 | 18 | return `is expected ${body.join(', ')}`; 19 | } 20 | 21 | function humanize(value) { 22 | return value.replace( 23 | /([a-z])([A-Z])/g, 24 | (_, before, letter) => `${before} ${letter.toLowerCase()}` 25 | ); 26 | } 27 | 28 | module.exports = { 29 | parseMessage, 30 | humanize, 31 | }; 32 | -------------------------------------------------------------------------------- /lib/suite_tracker.js: -------------------------------------------------------------------------------- 1 | const { Metadata } = require('./metadata'); 2 | 3 | class SuiteTracker { 4 | constructor(config = {}) { 5 | this.state = { currentlyDefinedSuite: config.rootSuite, contexts: [config.rootSuite] }; 6 | this.suiteTracker = config.suiteTracker; 7 | this.suites = []; 8 | this.cleanUpCurrentContext = this.cleanUpCurrentContext.bind(this); 9 | this.cleanUpCurrentAndRestorePrevContext = this.cleanUpCurrentAndRestorePrevContext.bind(this); 10 | } 11 | 12 | get currentContext() { 13 | return this.state.contexts[this.state.contexts.length - 1]; 14 | } 15 | 16 | get currentlyDefinedSuite() { 17 | return this.state.currentlyDefinedSuite; 18 | } 19 | 20 | wrapSuite(describe) { 21 | const tracker = this; 22 | 23 | return function detectSuite(title, defineTests, ...suiteArgs) { 24 | return describe(title, function defineSuite(...args) { 25 | tracker.trackSuite(this, defineTests, args); 26 | }, ...suiteArgs); 27 | }; 28 | } 29 | 30 | trackSuite(suite, defineTests, args) { 31 | const previousDefinedSuite = this.state.currentlyDefinedSuite; 32 | 33 | this.defineMetaFor(suite); 34 | this.state.currentlyDefinedSuite = suite; 35 | this.execute(defineTests, suite, args); 36 | this.state.currentlyDefinedSuite = previousDefinedSuite; 37 | } 38 | 39 | defineMetaFor(suite) { 40 | const meta = Metadata.ensureDefinedOn(suite); 41 | const parentMeta = Metadata.of(suite.parent || suite.parentSuite); 42 | 43 | if (parentMeta) { 44 | parentMeta.addChild(meta); 45 | } 46 | } 47 | 48 | execute(defineTests, suite, args) { 49 | this.suiteTracker.before(this, suite); 50 | defineTests.apply(suite, args); 51 | 52 | if (Metadata.of(suite)) { 53 | this.suiteTracker.after(this, suite); 54 | } 55 | } 56 | 57 | isRoot(suite) { 58 | return !(suite.parent ? suite.parent.parent : suite.parentSuite.parentSuite); 59 | } 60 | 61 | registerSuite(context) { 62 | this.state.contexts.push(context); 63 | } 64 | 65 | cleanUp(context) { 66 | const metadata = Metadata.of(context); 67 | 68 | if (metadata) { 69 | metadata.releaseVars(); 70 | } 71 | } 72 | 73 | cleanUpCurrentContext() { 74 | this.cleanUp(this.currentContext); 75 | } 76 | 77 | cleanUpCurrentAndRestorePrevContext() { 78 | this.cleanUpCurrentContext(); 79 | this.state.contexts.pop(); 80 | } 81 | } 82 | 83 | module.exports = SuiteTracker; 84 | -------------------------------------------------------------------------------- /lib/symbol.js: -------------------------------------------------------------------------------- 1 | const indentity = (x) => x; 2 | 3 | module.exports = { 4 | for: typeof Symbol === 'undefined' ? indentity : Symbol.for 5 | }; 6 | -------------------------------------------------------------------------------- /lib/variable.js: -------------------------------------------------------------------------------- 1 | const { Metadata } = require('./metadata'); 2 | const Symbol = require('./symbol'); 3 | 4 | const CURRENTLY_RETRIEVED_VAR_FIELD = Symbol.for('__currentVariableStack'); 5 | const last = (array) => array ? array[array.length - 1] : null; 6 | 7 | class Variable { 8 | static allocate(varName, options) { 9 | const variable = new this(varName, options.in); 10 | 11 | return variable.addToStack(); 12 | } 13 | 14 | static evaluate(varName, options) { 15 | if (!options.in) { 16 | throw new Error(`It looke like you are trying to evaluate "${varName}" too early. Evaluation context is undefined`); 17 | } 18 | 19 | let variable = Variable.fromStack(options.in); 20 | 21 | if (variable.isSame(varName)) { 22 | return variable.valueInParentContext(varName); 23 | } 24 | 25 | try { 26 | variable = Variable.allocate(varName, options); 27 | return variable.value(); 28 | } finally { 29 | variable.pullFromStack(); 30 | } 31 | } 32 | 33 | static fromStack(context) { 34 | return last(context[CURRENTLY_RETRIEVED_VAR_FIELD]) || Variable.EMPTY; 35 | } 36 | 37 | constructor(varName, context) { 38 | this.name = varName; 39 | this.context = context; 40 | this.evaluationMeta = context ? Metadata.of(context) : null; 41 | } 42 | 43 | isSame(anotherVarName) { 44 | return this.name && ( 45 | this.name === anotherVarName 46 | || Metadata.of(this.context, this.name).isNamedAs(anotherVarName) 47 | ); 48 | } 49 | 50 | value() { 51 | return this.evaluationMeta && this.evaluationMeta.getVar(this.name); 52 | } 53 | 54 | addToStack() { 55 | this.context[CURRENTLY_RETRIEVED_VAR_FIELD] = this.context[CURRENTLY_RETRIEVED_VAR_FIELD] || []; 56 | this.context[CURRENTLY_RETRIEVED_VAR_FIELD].push(this); 57 | 58 | return this; 59 | } 60 | 61 | pullFromStack() { 62 | this.context[CURRENTLY_RETRIEVED_VAR_FIELD].pop(); 63 | } 64 | 65 | valueInParentContext(varOrAliasName) { 66 | const meta = this.evaluationMeta; 67 | 68 | try { 69 | this.evaluationMeta = meta.lookupMetadataFor(varOrAliasName); 70 | return this.evaluationMeta.evaluate(varOrAliasName); 71 | } finally { 72 | this.evaluationMeta = meta; 73 | } 74 | } 75 | } 76 | 77 | Variable.EMPTY = new Variable(null, null); 78 | 79 | module.exports = Variable; 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bdd-lazy-var", 3 | "version": "2.6.1", 4 | "author": "Sergii Stotskyi", 5 | "description": "Provides \"ui\" for testing frameworks such as mocha/jasmine which allows to define lazy variables and subjects", 6 | "license": "MIT", 7 | "readmeFilename": "README.md", 8 | "repository": { 9 | "type": "git", 10 | "url": "git@github.com:stalniy/bdd-lazy-var.git" 11 | }, 12 | "keywords": [ 13 | "mocha.js", 14 | "jasmine", 15 | "jest", 16 | "bdd", 17 | "lazy", 18 | "variable", 19 | "syntax", 20 | "dsl", 21 | "subject", 22 | "rspec" 23 | ], 24 | "bugs": { 25 | "url": "https://github.com/stalniy/bdd-lazy-var/issues" 26 | }, 27 | "main": "./index", 28 | "peerDependencies": { 29 | "jasmine": ">=2", 30 | "jasmine-core": ">=2", 31 | "jest": ">=20", 32 | "mocha": ">=2.3" 33 | }, 34 | "peerDependenciesMeta": { 35 | "jasmine": { 36 | "optional": true 37 | }, 38 | "jasmine-core": { 39 | "optional": true 40 | }, 41 | "jest": { 42 | "optional": true 43 | }, 44 | "mocha": { 45 | "optional": true 46 | } 47 | }, 48 | "devDependencies": { 49 | "@babel/core": "^7.2.2", 50 | "@babel/plugin-proposal-object-rest-spread": "^7.3.2", 51 | "@babel/plugin-transform-object-assign": "^7.2.0", 52 | "@babel/preset-env": "^7.3.1", 53 | "@semantic-release/changelog": "^6.0.0", 54 | "@semantic-release/git": "^10.0.0", 55 | "babel-plugin-transform-object-assign": "^6.22.0", 56 | "chai": "^5.0.0", 57 | "chai-spies": "^1.0.0", 58 | "eslint": "^8.0.0", 59 | "eslint-config-airbnb-base": "^15.0.0", 60 | "eslint-plugin-import": "^2.20.1", 61 | "jasmine": "^5.0.0", 62 | "jest": "^30.0.0", 63 | "karma": "^6.0.0", 64 | "karma-chrome-launcher": "^3.1.0", 65 | "karma-jasmine": "^5.0.0", 66 | "karma-mocha": "^2.0.0", 67 | "mocha": "^11.0.0", 68 | "puppeteer": "^24.0.0", 69 | "rollup": "^4.0.0", 70 | "@rollup/plugin-babel": "^6.0.0", 71 | "rollup-plugin-commonjs": "^10.0.0", 72 | "rollup-plugin-inject": "^3.0.0", 73 | "semantic-release": "^24.0.0" 74 | }, 75 | "scripts": { 76 | "mocha": "NODE_PATH=. mocha -r spec/config spec/*_examples.js spec/shared_behavior_spec.js", 77 | "test.mocha-ui": "npm run mocha -- -u index.js spec/interface_spec.js", 78 | "test.mocha-global": "npm run mocha -- -u global.js spec/global_defs_spec.js", 79 | "test.mocha-getter": "npm run mocha -- -u getter.js spec/getter_defs_spec.js", 80 | "test.mocha": "npm run test.mocha-ui && npm run test.mocha-global && npm run test.mocha-getter", 81 | "test.browser-mocha-ui": "karma start -u bdd-lazy-var --src index.js --specs spec/interface_spec.js tools/karma.config.js", 82 | "test.browser-mocha-global": "karma start -u bdd-lazy-var/global --src global.js --specs spec/global_defs_spec.js,spec/interface_spec.js tools/karma.config.js", 83 | "test.browser-mocha-getter": "karma start -u bdd-lazy-var/getter --src getter.js --specs spec/getter_defs_spec.js,spec/interface_spec.js tools/karma.config.js", 84 | "test.mocha-in-browser": "npm run test.browser-mocha-ui && npm run test.browser-mocha-global && npm run test.browser-mocha-getter", 85 | "test.jasmine-ui": "node tools/jasmine index.js interface_spec.js", 86 | "test.jasmine-global": "node tools/jasmine global.js global_defs_spec.js", 87 | "test.jasmine-getter": "node tools/jasmine getter.js getter_defs_spec.js", 88 | "test.jasmine": "npm run test.jasmine-ui && npm run test.jasmine-global && npm run test.jasmine-getter", 89 | "test.browser-jasmine-ui": "karma start -f jasmine --src index.js --specs spec/interface_spec.js tools/karma.config.js", 90 | "test.browser-jasmine-global": "karma start -f jasmine --src global.js --specs spec/global_defs_spec.js,spec/interface_spec.js tools/karma.config.js", 91 | "test.browser-jasmine-getter": "karma start -f jasmine --src getter.js --specs spec/getter_defs_spec.js,spec/interface_spec.js tools/karma.config.js", 92 | "test.jasmine-in-browser": "npm run test.browser-jasmine-ui && npm run test.browser-jasmine-global && npm run test.browser-jasmine-getter", 93 | "test.jest-ui": "SRC_FILE=index.js jest --findRelatedTests spec/interface_spec.js", 94 | "test.jest-global": "SRC_FILE=global.js jest --findRelatedTests spec/global_defs_spec.js", 95 | "test.jest-getter": "SRC_FILE=getter.js jest --findRelatedTests spec/getter_defs_spec.js", 96 | "test.jest": "npm run test.jest-ui && npm run test.jest-global && npm run test.jest-getter", 97 | "test": "npm run test.mocha && npm run test.mocha-in-browser && npm run test.jasmine && npm run test.jasmine-in-browser && npm run test.jest", 98 | "prebuild": "npm run lint", 99 | "build.ui": "rollup -c tools/rollup.umd.js -i lib/interface/dialects/bdd.js -o index.js", 100 | "build.global-ui": "rollup -c tools/rollup.umd.js -i lib/interface/dialects/bdd_global_var.js -o global.js", 101 | "build.getter-ui": "rollup -c tools/rollup.umd.js -i lib/interface/dialects/bdd_getter_var.js -o getter.js", 102 | "build": "npm run build.ui && npm run build.global-ui && npm run build.getter-ui", 103 | "lint": "eslint --fix lib", 104 | "prerelease": "NODE_ENV=production npm run build", 105 | "release": "semantic-release" 106 | }, 107 | "jest": { 108 | "testEnvironment": "node", 109 | "testMatch": [ 110 | "/spec/**/*_spec.js" 111 | ], 112 | "setupFilesAfterEnv": [ 113 | "./tools/jest.setup.js" 114 | ] 115 | }, 116 | "files": [ 117 | "getter.js", 118 | "getter.js.map", 119 | "getter.d.ts", 120 | "global.js", 121 | "global.js.map", 122 | "global.d.ts", 123 | "index.js", 124 | "index.js.map", 125 | "index.d.ts", 126 | "interface.d.ts" 127 | ], 128 | "release": { 129 | "branches": [ 130 | "master" 131 | ], 132 | "plugins": [ 133 | [ 134 | "@semantic-release/commit-analyzer", 135 | { 136 | "releaseRules": [ 137 | { 138 | "type": "chore", 139 | "scope": "deps", 140 | "release": "patch" 141 | }, 142 | { 143 | "type": "docs", 144 | "scope": "README", 145 | "release": "patch" 146 | } 147 | ] 148 | } 149 | ], 150 | "@semantic-release/release-notes-generator", 151 | "@semantic-release/changelog", 152 | "@semantic-release/npm", 153 | "@semantic-release/git", 154 | "@semantic-release/github" 155 | ] 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /spec/config.js: -------------------------------------------------------------------------------- 1 | (function(factory) { 2 | if (typeof require === 'function' && typeof module !== 'undefined') { 3 | require('chai').use(require('chai-spies')); 4 | factory(require('chai'), this); 5 | } else if (typeof window === 'object') { 6 | window.global = window; 7 | factory(window.chai, window); 8 | } 9 | })(function(chai, global) { 10 | global.expect = chai.expect; 11 | global.spy = chai.spy; 12 | 13 | if (global.beforeAll) { 14 | global.before = global.beforeAll; 15 | } 16 | 17 | if (global.afterAll) { 18 | global.after = global.afterAll; 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /spec/default_suite_tracking_examples.js: -------------------------------------------------------------------------------- 1 | sharedExamplesFor('Default suite tracking', function(getVar) { 2 | describe('when using variable inside another variable definition', function() { 3 | var user = { firstName: 'John', lastName: 'Doe' }; 4 | var index = 0; 5 | var currentIndex; 6 | 7 | before(function() { 8 | expect(getVar('currentIndex')).to.equal(currentIndex); 9 | }); 10 | 11 | beforeEach(function usesVariableDefinedInCurrentlyRunningSuiteInBeforeEachCallback() { 12 | expect(cast(getVar('currentIndex'))).to.equal(cast(currentIndex)); 13 | }); 14 | 15 | afterEach(function usesVariableDefinedInCurrentlyRunningSuiteInAfterEachCallback() { 16 | expect(cast(getVar('currentIndex'))).to.equal(cast(currentIndex)); 17 | }); 18 | 19 | after(function usesOwnDefinedVariable() { 20 | expect(getVar('currentIndex')).to.equal(currentIndex); 21 | }); 22 | 23 | def('personName', function() { 24 | return getVar('firstName') + ' ' + getVar('lastName'); 25 | }); 26 | 27 | def('firstName', user.firstName); 28 | def('lastName', user.lastName); 29 | 30 | def('currentIndex', function() { 31 | currentIndex = ++index; 32 | 33 | return currentIndex; 34 | }); 35 | 36 | def('CurrenIndexType', function() { 37 | return Number; 38 | }); 39 | 40 | it('computes the proper result', function() { 41 | expect(getVar('personName')).to.equal(user.firstName + ' ' + user.lastName); 42 | }); 43 | 44 | describe('nested suite', function() { 45 | var user = { firstName: 'Alex' }; 46 | 47 | before(function() { 48 | expect(getVar('currentIndex')).to.equal(cast(currentIndex)); 49 | }); 50 | 51 | beforeEach(function usesOwnDefinedVariableInBeforeEachCallbackEvenWhenItIsRunForNestedTests() { 52 | expect(getVar('currentIndex')).to.equal(cast(currentIndex)); 53 | }); 54 | 55 | afterEach(function usesOwnDefinedVariableInAfterEachCallbackEvenWhenItIsRunForNestedTests() { 56 | expect(getVar('currentIndex')).to.equal(cast(currentIndex)); 57 | }); 58 | 59 | after(function usesOwnDefinedVariable() { 60 | expect(getVar('currentIndex')).to.equal(cast(currentIndex)); 61 | }); 62 | 63 | def('firstName', user.firstName); 64 | 65 | def('currentIndex', function() { 66 | return cast(getVar('currentIndex')); 67 | }); 68 | 69 | def('CurrenIndexType', function() { 70 | return String; 71 | }); 72 | 73 | it('falls back to parent variable', function() { 74 | expect(getVar('lastName')).to.equal('Doe'); 75 | }); 76 | 77 | it('computes parent variable using redefined variable', function() { 78 | expect(getVar('personName')).to.equal(user.firstName + ' ' + getVar('lastName')); 79 | }); 80 | 81 | it('can redefine parent variable with the same name and access value of parent variable inside definition', function() { 82 | expect(getVar('currentIndex')).to.equal(cast(currentIndex)); 83 | }); 84 | }); 85 | 86 | function cast(value) { 87 | var convert = getVar('CurrenIndexType'); 88 | 89 | return convert(value); 90 | } 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /spec/getter_defs_spec.js: -------------------------------------------------------------------------------- 1 | function getVar(name) { 2 | return get[name]; 3 | } 4 | 5 | includeExamplesFor('Root Lazy Vars', getVar); 6 | 7 | describe('Lazy vars defined as getter on "get" function', function() { 8 | includeExamplesFor('Lazy Vars Interface', getVar); 9 | 10 | subject(function() { 11 | return {}; 12 | }); 13 | 14 | describe('by default', function() { 15 | subject(function() { 16 | return {}; 17 | }); 18 | 19 | def('firstName', 'John'); 20 | def('anotherVar', 'Doe'); 21 | 22 | try { 23 | get.bddLazyCounter = 2; 24 | def('bddLazyCounter', 5); 25 | } catch(e) { 26 | get.bddLazyCounter = null; 27 | } 28 | 29 | it('defines a getter for lazy variable', function() { 30 | expect(get.subject).to.exist; 31 | }); 32 | 33 | it('allows to access lazy variable value by checking property on "get" function', function() { 34 | expect(get.subject).to.equal(subject()); 35 | }); 36 | 37 | it('forwards calls to `get` function when access variable', function() { 38 | var accessor = spy(); 39 | var originalGet = global.get; 40 | 41 | global.get = accessor; 42 | originalGet.anotherVar; 43 | global.get = originalGet; 44 | 45 | expect(accessor).to.have.been.called.with('anotherVar'); 46 | }); 47 | 48 | it('does not allow to redefine existing variable in global context', function() { 49 | expect(get.bddLazyCounter).to.be.null; 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /spec/global_defs_spec.js: -------------------------------------------------------------------------------- 1 | function getVar(name) { 2 | return global['$' + name]; 3 | } 4 | 5 | includeExamplesFor('Root Lazy Vars', getVar); 6 | 7 | describe('Interface with globally defined lazy vars', function() { 8 | includeExamplesFor('Lazy Vars Interface', getVar); 9 | includeExamplesFor('Default suite tracking', getVar); 10 | 11 | describe('by default', function() { 12 | subject(function() { 13 | return {}; 14 | }); 15 | 16 | def('firstName', 'John'); 17 | def('anotherVar', 'Doe'); 18 | 19 | try { 20 | global.$bddLazyCounter = 2; 21 | def('bddLazyCounter', 5); 22 | } catch(e) { 23 | global.$bddLazyCounter = null; 24 | } 25 | 26 | it('defines a getter on global object for lazy variable with name prefixed by "$"', function() { 27 | expect(global.$subject).to.exist; 28 | }); 29 | 30 | it('allows to access lazy variable value by its name', function() { 31 | expect($subject).to.equal(subject()); 32 | }); 33 | 34 | it('forwards calls to `get` function when access variable', function() { 35 | var accessor = spy(); 36 | var originalGet = global.get; 37 | 38 | global.get = accessor; 39 | $anotherVar; 40 | global.get = originalGet; 41 | 42 | expect(accessor).to.have.been.called.with('anotherVar'); 43 | }); 44 | 45 | it('does not allow to redefine existing variable in global context', function() { 46 | expect($bddLazyCounter).to.be.null; 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /spec/interface_examples.js: -------------------------------------------------------------------------------- 1 | sharedExamplesFor('Lazy Vars Interface', function(getVar) { 2 | describe('by default', function() { 3 | var definition; 4 | var value = {}; 5 | 6 | def('var', function() { 7 | return definition(); 8 | }); 9 | def('staticVar', value); 10 | 11 | def('fullName', function() { 12 | return getVar('firstName') + ' ' + getVar('lastName'); 13 | }); 14 | 15 | def('firstName', 'John'); 16 | def('lastName', 'Doe'); 17 | 18 | beforeEach(function() { 19 | definition = spy(); 20 | }) 21 | 22 | it('does not create variable if it has not been accessed', function() { 23 | expect(definition).not.to.have.been.called(); 24 | }); 25 | 26 | it('creates variable only once', function() { 27 | getVar('var'); 28 | getVar('var'); 29 | 30 | expect(definition).to.have.been.called.once; 31 | }); 32 | 33 | it('can define static variable', function() { 34 | expect(getVar('staticVar')).to.equal(value); 35 | }); 36 | 37 | it('returns `undefined` where there is no definition', () => { 38 | expect(getVar('notDefined')).to.be.undefined; 39 | }); 40 | 41 | it('defines "get.variable" and its alias "get.definitionOf" getter builder', function() { 42 | expect(get.variable).to.be.a('function'); 43 | expect(get.variable).to.equal(get.definitionOf); 44 | }); 45 | 46 | it('allows to get variable using builder', function() { 47 | var getStatic = get.variable('staticVar'); 48 | 49 | expect(getStatic()).to.equal(getVar('staticVar')); 50 | }); 51 | 52 | describe('nested suite', function() { 53 | def('lastName', 'Smith'); 54 | 55 | it('uses suite specific variable inside dynamic parent variable', function() { 56 | expect(getVar('fullName')).to.equal('John Smith'); 57 | }); 58 | }); 59 | 60 | context('nested suite using \'context\' alias', function() { 61 | def('lastName', 'Cusak'); 62 | 63 | it('uses suite specific variable inside dynamic parent variable', function() { 64 | expect(getVar('fullName')).to.equal('John Cusak'); 65 | }); 66 | }); 67 | 68 | try { 69 | xcontext('skipped context', function() { 70 | it('should never call assertions', function() { 71 | is.expected.to.be.never.called(); 72 | }); 73 | }); 74 | } catch (error) { 75 | it(function() { 76 | is.expected.to.be.never.called(); 77 | }); 78 | } 79 | }); 80 | 81 | describe('dynamic variable definition', function() { 82 | var prevValue, valueInAfterEach, valueInBefore, valueInFirstBeforeEach, skipBeforeEach; 83 | var index = 0; 84 | 85 | def('var', function() { 86 | prevValue = index; 87 | 88 | return ++index; 89 | }); 90 | 91 | before(function() { 92 | valueInBefore = getVar('var'); 93 | }); 94 | 95 | beforeEach(function() { 96 | if (!skipBeforeEach) { 97 | skipBeforeEach = true; 98 | valueInFirstBeforeEach = getVar('var'); 99 | } 100 | }); 101 | 102 | afterEach(function usesCachedVariable() { 103 | valueInAfterEach = getVar('var'); 104 | 105 | expect(getVar('var')).to.equal(prevValue + 1); 106 | }); 107 | 108 | after(function usesNewlyCreatedVariable() { 109 | expect(getVar('var')).to.equal(valueInAfterEach + 1); 110 | }); 111 | 112 | it('defines dynamic variable', function() { 113 | expect(getVar('var')).to.exist; 114 | }); 115 | 116 | it('stores different values between tests', function() { 117 | expect(getVar('var')).to.equal(prevValue + 1); 118 | }); 119 | 120 | it('does not share the same value between "before" and first "beforeEach" calls', function() { 121 | expect(valueInBefore).not.to.equal(valueInFirstBeforeEach); 122 | }); 123 | }); 124 | 125 | describe('when fallbacks to parent variable definition through suites tree', function() { 126 | def('var', 'Doe'); 127 | 128 | describe('nested suite without variable definition', function() { 129 | def('hasVariables', true); 130 | 131 | it('fallbacks to parent variable definition', function() { 132 | expect(getVar('var')).to.equal('Doe'); 133 | }); 134 | 135 | it('can define other variables inside', function() { 136 | expect(getVar('hasVariables')).to.be.true; 137 | }) 138 | 139 | describe('nested suite with variable definition', function() { 140 | def('var', function() { 141 | return get('anotherVar') + ' ' + getVar('var'); 142 | }); 143 | 144 | def('anotherVar', function() { 145 | return 'John'; 146 | }); 147 | 148 | it('uses correct parent variable definition', function() { 149 | expect(getVar('var')).to.equal('John Doe'); 150 | }); 151 | 152 | describe('one more nested suite without variable definition', function() { 153 | it('uses correct parent variable definition', function() { 154 | expect(getVar('var')).to.equal('John Doe'); 155 | }); 156 | }); 157 | }); 158 | }); 159 | }); 160 | 161 | describe('when variable is used inside "afterEach" of parent and child suites', function() { 162 | var subjectInChild; 163 | 164 | subject(function() { 165 | return {}; 166 | }); 167 | 168 | describe('parent suite', function() { 169 | afterEach(function() { 170 | expect(subject()).to.equal(subjectInChild); 171 | }); 172 | 173 | describe('child suite', function() { 174 | it('uses the same variable instance', function() { 175 | subjectInChild = subject(); 176 | }); 177 | }); 178 | }); 179 | }); 180 | 181 | describe('named subject', function() { 182 | var subjectValue = {}; 183 | 184 | subject('named', subjectValue); 185 | 186 | it('is accessible by referencing "subject" variable', function() { 187 | expect(getVar('subject')).to.equal(subjectValue); 188 | }); 189 | 190 | it('is accessible by referencing subject name variable', function() { 191 | expect(getVar('named')).to.equal(subjectValue); 192 | }); 193 | 194 | describe('nested suite', function() { 195 | var nestedSubjectValue = {}; 196 | 197 | subject('nested', nestedSubjectValue); 198 | 199 | it('shadows parent "subject" variable', function() { 200 | expect(getVar('subject')).to.equal(nestedSubjectValue); 201 | }); 202 | 203 | it('can access parent subject by its name', function() { 204 | expect(getVar('named')).to.equal(subjectValue); 205 | }); 206 | }); 207 | 208 | describe('parent subject in child one', function() { 209 | subject('nested', function() { 210 | return getVar('subject'); 211 | }); 212 | 213 | it('can access parent subject inside named subject by accessing "subject" variable', function() { 214 | expect(getVar('subject')).to.equal(subjectValue); 215 | }); 216 | 217 | it('can access parent subject inside named subject by accessing subject by its name', function() { 218 | expect(getVar('nested')).to.equal(subjectValue); 219 | }); 220 | }); 221 | }); 222 | 223 | describe('variables in skipped suite', function() { 224 | subject([]); 225 | 226 | xdescribe('Skipped suite', function() { 227 | var object = {}; 228 | 229 | subject(object); 230 | 231 | it('defines variables inside skipped suites', function() { 232 | expect(getVar('subject')).to.equal(object); 233 | }); 234 | }); 235 | }); 236 | 237 | describe('referencing child lazy variable from parent', function() { 238 | def('model', function() { 239 | return { value: getVar('value') }; 240 | }); 241 | 242 | describe('nested suite', function() { 243 | subject(function() { 244 | return getVar('model').value; 245 | }); 246 | 247 | describe('suite which defines variable used in parent suite', function() { 248 | def('value', function() { 249 | return { x: 5 }; 250 | }); 251 | 252 | subject(function() { 253 | return getVar('subject').x; 254 | }); 255 | 256 | it('returns 5', function() { 257 | expect(getVar('subject')).to.equal(5); 258 | }); 259 | }); 260 | }); 261 | }); 262 | 263 | describe('when parent variable is accessed multiple times inside child definition', function() { 264 | subject(function() { 265 | return { isParent: true, name: 'test' }; 266 | }); 267 | 268 | describe('child suite', function() { 269 | subject(function() { 270 | return { 271 | isParent: !subject().isParent, 272 | name: subject().name + ' child' 273 | }; 274 | }); 275 | 276 | it('retrieves proper parent variable', function() { 277 | expect(subject().isParent).to.be.false; 278 | expect(subject().name).to.equal('test child'); 279 | }); 280 | }); 281 | }); 282 | 283 | describe('when calls variable defined in parent suites', function() { 284 | subject(function() { 285 | return { isRoot: getVar('isRoot') }; 286 | }); 287 | 288 | def('isRoot', true); 289 | 290 | describe('one more level which overrides parent variable', function() { 291 | subject(function() { 292 | return getVar('subject').isRoot; 293 | }); 294 | 295 | describe('suite that calls parent variable and redefines dependent variable', function() { 296 | def('isRoot', false); 297 | 298 | it('gets the correct variable', function() { 299 | expect(getVar('subject')).to.be.false; 300 | }); 301 | }); 302 | 303 | describe('suite that calls parent variable', function() { 304 | it('gets the correct variable', function() { 305 | expect(getVar('subject')).to.be.true; 306 | }); 307 | }); 308 | }); 309 | }); 310 | 311 | describe('`its`', function() { 312 | subject(function() { 313 | return { 314 | value: 5, 315 | nested: { 316 | value: 10 317 | }, 318 | getName() { 319 | return 'John' 320 | } 321 | }; 322 | }); 323 | 324 | its('value', function() { 325 | is.expected.to.equal(getVar('subject').value); 326 | }); 327 | 328 | its('getName', function() { 329 | is.expected.to.equal(getVar('subject').getName()); 330 | }); 331 | 332 | its('nested.value', function() { 333 | is.expected.to.equal(getVar('subject').nested.value); 334 | }); 335 | 336 | try { 337 | its.skip('name', function() { 338 | is.expected.to.be.never.called(); 339 | }); 340 | } catch (error) { 341 | xits('name', function() { 342 | is.expected.to.be.never.called(); 343 | }); 344 | } 345 | }); 346 | }); 347 | 348 | sharedExamplesFor('Root Lazy Vars', function(getVar) { 349 | const varName = `hello.${Date.now()}.${Math.random()}` 350 | 351 | def(varName, function() { 352 | return 'world' 353 | }); 354 | 355 | it('allows to define lazy vars at root level', function() { 356 | expect(getVar(varName)).to.equal('world'); 357 | }); 358 | }); 359 | -------------------------------------------------------------------------------- /spec/interface_spec.js: -------------------------------------------------------------------------------- 1 | includeExamplesFor('Root Lazy Vars', get); 2 | 3 | describe('Lazy variables interface', function() { 4 | includeExamplesFor('Lazy Vars Interface', get); 5 | includeExamplesFor('Default suite tracking', get); 6 | 7 | describe('`it` without message', function() { 8 | subject(function() { 9 | return { 10 | items: [1, 2, 3] 11 | }; 12 | }); 13 | 14 | it(function() { 15 | is.expected.to.be.an('object'); 16 | }); 17 | 18 | it(function() { 19 | is.expected.to.have.property('items').which.has.length(3); 20 | }); 21 | 22 | try { 23 | it.skip(function() { 24 | is.expected.to.be.never.called(); 25 | }); 26 | } catch (error) { 27 | xit(function() { 28 | is.expected.to.be.never.called(); 29 | }); 30 | } 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /spec/shared_behavior_spec.js: -------------------------------------------------------------------------------- 1 | describe('Shared behavior', function() { 2 | describe('`sharedExamplesFor`', function() { 3 | var defineError; 4 | 5 | sharedExamplesFor('__test', function() {}); 6 | 7 | try { 8 | sharedExamplesFor('__test', function() {}); 9 | } catch (error) { 10 | defineError = error; 11 | } 12 | 13 | it('throws error when trying to redefine existing shared examples', function() { 14 | expect(defineError.message).to.match(/Attempt to override/); 15 | }); 16 | }); 17 | 18 | describe('`includeExamplesFor`', function() { 19 | var includeError; 20 | var examples = spy(); 21 | var args = [{}, {}]; 22 | var fnDefinition = spy(); 23 | 24 | try { 25 | includeExamplesFor('__non_existing') 26 | } catch (error) { 27 | includeError = error; 28 | } 29 | 30 | sharedExamplesFor('__call', examples); 31 | includeExamplesFor('__call', args[0], args[1]); 32 | includeExamplesFor(fnDefinition, args[0]); 33 | 34 | it('throws error when trying to include non-existing shared examples', function() { 35 | expect(includeError.message).to.match(/not defined shared behavior/) 36 | }); 37 | 38 | it('calls registered shared examples with specified arguments', function() { 39 | expect(examples).to.have.been.called.with.exactly(args[0], args[1]); 40 | }); 41 | 42 | it('accepts function as the 1st argument and call it', () => { 43 | expect(fnDefinition).to.have.been.called.with.exactly(args[0]); 44 | }) 45 | }); 46 | 47 | describe('`itBehavesLike`', function() { 48 | var examples = spy(); 49 | var args = [{}, {}]; 50 | var spiedDescribe = spy.on(global, 'describe'); 51 | var fnBehavior = spy(); 52 | 53 | sharedExamplesFor('__Collection', examples); 54 | itBehavesLike('__Collection', args[0], args[1]); 55 | spy.restore(global, 'describe'); 56 | 57 | itBehavesLike(fnBehavior, args[0]); 58 | 59 | it('includes examples in a nested context', function() { 60 | expect(spiedDescribe).to.have.been.called.with('behaves like __Collection'); 61 | expect(examples).to.have.been.called.with.exactly(args[0], args[1]); 62 | }); 63 | 64 | it('accepts behavior defined in function', function () { 65 | expect(fnBehavior).to.have.been.called.with(args[0]); 66 | }); 67 | }); 68 | 69 | describe('`sharedExamplesFor` scoping', function() { 70 | var isExamplesProperlyDefined; 71 | 72 | describe('suite with `sharedExamplesFor(__test__)`', function() { 73 | sharedExamplesFor('__test__', function() { 74 | isExamplesProperlyDefined = true; 75 | }); 76 | includeExamplesFor('__test__'); 77 | }); 78 | 79 | describe('tests', function() { 80 | var missedError; 81 | 82 | try { 83 | includeExamplesFor('__test__'); 84 | } catch (error) { 85 | missedError = error; 86 | } 87 | 88 | it('defines examples scoped to the suite tree', function() { 89 | expect(isExamplesProperlyDefined).to.be.true; 90 | expect(missedError).to.match(/not defined shared behavior/) 91 | }); 92 | }) 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /tools/globals/global.js: -------------------------------------------------------------------------------- 1 | export default typeof global !== "undefined" ? global : 2 | typeof self !== "undefined" ? self : 3 | typeof window !== "undefined" ? window : {} 4 | -------------------------------------------------------------------------------- /tools/jasmine.js: -------------------------------------------------------------------------------- 1 | const JasmineCli = require('jasmine'); 2 | 3 | const jasmine = new JasmineCli(); 4 | const helpers = [ 5 | '../spec/config', 6 | `../${process.argv[2]}` 7 | ]; 8 | 9 | helpers.forEach(require); 10 | jasmine.loadConfig({ 11 | spec_dir: 'spec', 12 | spec_files: [ 13 | 'interface_examples.js', 14 | 'default_suite_tracking_examples.js', 15 | 'shared_behavior_spec.js' 16 | ].concat(process.argv.slice(3)), 17 | }); 18 | 19 | jasmine.execute(); 20 | -------------------------------------------------------------------------------- /tools/jest.setup.js: -------------------------------------------------------------------------------- 1 | const uiFile = process.env.SRC_FILE; 2 | 3 | require('../spec/config'); 4 | require(`../${uiFile}`); 5 | require('../spec/interface_examples'); 6 | require('../spec/default_suite_tracking_examples'); 7 | require('../spec/shared_behavior_spec'); 8 | -------------------------------------------------------------------------------- /tools/karma.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const puppeteer = require('puppeteer'); 3 | 4 | process.env.CHROME_BIN = puppeteer.executablePath(); 5 | 6 | module.exports = function(config) { 7 | const specs = (config.specs || '').split(','); 8 | const srcFiles = (config.src || '').split(','); 9 | const frameworks = (config.f || 'mocha').split(','); 10 | 11 | srcFiles.unshift( 12 | 'node_modules/chai/chai.js', 13 | 'node_modules/chai-spies/chai-spies.js', 14 | 'spec/config.js' 15 | ); 16 | specs.unshift( 17 | 'spec/interface_examples.js', 18 | 'spec/default_suite_tracking_examples.js' 19 | ); 20 | 21 | config.set({ 22 | frameworks, 23 | basePath: '..', 24 | reporters: ['dots'], 25 | autoWatch: false, 26 | singleRun: true, 27 | browsers: ['ChromeHeadless'], 28 | files: frameworks.includes('mocha') ? specs : srcFiles.concat(specs), 29 | client: { 30 | mocha: { 31 | ui: config.u, 32 | require: srcFiles.reverse().map(filePath => path.resolve(filePath)) 33 | } 34 | } 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /tools/rollup.umd.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import inject from 'rollup-plugin-inject'; 4 | import { resolve as resolvePath } from 'path'; 5 | 6 | const MODULE_NAME = 'bdd_lazy_var'; 7 | 8 | function dontPoluteGlobal() { 9 | return { 10 | name: 'no-global-polution', 11 | renderChunk(source) { 12 | const regexp = new RegExp(`global.${MODULE_NAME} *= *(factory.+)`); 13 | return source.replace(regexp, '$1'); 14 | } 15 | } 16 | } 17 | 18 | function useSafeDependencies(deps) { 19 | return { 20 | name: 'safe-deps', 21 | renderChunk(source) { 22 | return source 23 | .replace(/(function *\([^)]*\) \{)/, [ 24 | '$1', 25 | 'function optional(name) { try { return require(name) } catch(e) {} }' 26 | ].join('\n')) 27 | .replace(new RegExp(`require\\(["'](${deps.join('|')})["']\\)`, 'g'), 'optional("$1")') 28 | .replace(/define\(\[([^\]]+)\]/, (match, names) => { 29 | const allDeps = names.split(/\s*,\s*/).map((name) => { 30 | const depName = name.slice(1, -1); 31 | return deps.includes(depName) ? `'optional!${depName}'` : name; 32 | }); 33 | 34 | return `define([${allDeps.join(', ')}]` 35 | }) 36 | } 37 | } 38 | } 39 | 40 | export default { 41 | external: ['mocha', 'jasmine', 'jest'], 42 | output: { 43 | format: 'umd', 44 | name: MODULE_NAME, 45 | sourcemap: true, 46 | globals: { 47 | mocha: 'Mocha', 48 | jasmine: 'jasmine', 49 | jest: 'jest' 50 | }, 51 | }, 52 | plugins: [ 53 | commonjs({ 54 | include: 'lib/**', 55 | ignoreGlobal: true 56 | }), 57 | inject({ 58 | include: 'lib/**', 59 | global: resolvePath('./tools/globals/global.js') 60 | }), 61 | babel({ 62 | exclude: 'node_modules/**', 63 | presets: [ 64 | ['@babel/preset-env', { 65 | modules: false, 66 | loose: true, 67 | targets: { 68 | browsers: ['last 3 versions', 'safari >= 7'] 69 | } 70 | }] 71 | ], 72 | plugins: [ 73 | '@babel/plugin-transform-object-assign', 74 | '@babel/plugin-proposal-object-rest-spread' 75 | ] 76 | }), 77 | dontPoluteGlobal(), 78 | useSafeDependencies([ 79 | 'mocha', 80 | 'jasmine' 81 | ]) 82 | ] 83 | }; 84 | --------------------------------------------------------------------------------