├── .npmrc ├── .eslintignore ├── test ├── jest │ ├── .eslintrc │ ├── withGlobal.js │ ├── withOverride.js │ ├── withOverrides.js │ └── core.js ├── jest-only │ ├── .eslintrc │ └── index.js ├── .eslintrc └── tape │ └── index.js ├── .eslintrc ├── .github └── workflows │ ├── require-allow-edits.yml │ ├── rebase.yml │ ├── node-pretest.yml │ └── node-4+.yml ├── helpers └── checkWithName.js ├── withGlobal.js ├── withOverride.js ├── .gitignore ├── LICENSE ├── .istanbul.yml ├── CHANGELOG.md ├── withOverrides.js ├── package.json ├── README.md └── index.js /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /test/jest/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /test/jest-only/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | 4 | "extends": "airbnb-base/legacy", 5 | 6 | "rules": { 7 | "dot-notation": [2, { "allowKeywords": false }], 8 | "func-names": 0, 9 | "indent": [2, "tab"], 10 | "no-tabs": 0, 11 | "vars-on-top": 0, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/require-allow-edits.yml: -------------------------------------------------------------------------------- 1 | name: Require “Allow Edits” 2 | 3 | on: [pull_request_target] 4 | 5 | jobs: 6 | _: 7 | name: "Require “Allow Edits”" 8 | 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: ljharb/require-allow-edits@main 13 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "array-bracket-newline": 0, 4 | "array-element-newline": 0, 5 | "indent": [2, 'tab', { "MemberExpression": 0 }], 6 | "max-params": 0, 7 | "max-nested-callbacks": [2, 5], 8 | "max-statements": 0, 9 | "max-statements-per-line": 0, 10 | "object-curly-newline": 0, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/rebase.yml: -------------------------------------------------------------------------------- 1 | name: Automatic Rebase 2 | 3 | on: [pull_request_target] 4 | 5 | jobs: 6 | _: 7 | name: "Automatic Rebase" 8 | 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: ljharb/rebase@master 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /test/jest-only/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var wrap = require('../../'); 5 | 6 | var withNothing = function withNothing() { 7 | return this.extend('with nothing', { beforeAll: function () {} }); 8 | }; 9 | 10 | describe('#only()', function () { 11 | it('fails', function () { 12 | assert.equal(true, false, 'explode!'); 13 | }); 14 | 15 | wrap().use(withNothing).only().it('passes', function () { 16 | assert.equal(true, true, 'testing is fun'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /helpers/checkWithName.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bind = require('function-bind'); 4 | var isString = require('is-string'); 5 | 6 | var withRegex = /^with[^\s\n]+$/; 7 | var validWithName = bind.call(withRegex.test, withRegex); 8 | 9 | module.exports = function checkWithName(name) { 10 | if (!isString(name) || name.length === 0) { 11 | throw new TypeError('withName must be a non-empty string'); 12 | } 13 | if (!validWithName(name)) { 14 | throw new TypeError('withName must start with "with" and contain no whitespace'); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /withGlobal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var isString = require('is-string'); 4 | var isSymbol = require('is-symbol'); 5 | var inspect = require('object-inspect'); 6 | var withOverride = require('./withOverride'); 7 | 8 | var getGlobal = function () { return global; }; 9 | 10 | module.exports = function withGlobal(globalName, valueThunk) { 11 | var isNonEmptyString = isString(globalName) && globalName.length > 0; 12 | if (!isNonEmptyString && !isSymbol(globalName)) { 13 | throw new TypeError('global name must be a non-empty string or a Symbol'); 14 | } 15 | 16 | return this.use(withOverride, getGlobal, globalName, valueThunk).extend('with global: ' + inspect(globalName)); 17 | }; 18 | -------------------------------------------------------------------------------- /.github/workflows/node-pretest.yml: -------------------------------------------------------------------------------- 1 | name: 'Tests: pretest/posttest' 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | pretest: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: ljharb/actions/node/install@main 12 | name: 'nvm install lts/* && npm install' 13 | with: 14 | node-version: 'lts/*' 15 | - run: npm run pretest 16 | 17 | posttest: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: ljharb/actions/node/install@main 23 | name: 'nvm install lts/* && npm install' 24 | with: 25 | node-version: 'lts/*' 26 | - run: npm run posttest 27 | -------------------------------------------------------------------------------- /withOverride.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var isCallable = require('is-callable'); 4 | var isString = require('is-string'); 5 | var isSymbol = require('is-symbol'); 6 | var inspect = require('object-inspect'); 7 | var withOverrides = require('./withOverrides'); 8 | 9 | module.exports = function withOverride(objectThunk, key, valueThunk) { 10 | if (!isString(key) && !isSymbol(key)) { 11 | throw new TypeError('override key must be a string or a Symbol'); 12 | } 13 | if (!isCallable(valueThunk)) { 14 | throw new TypeError('a function that returns the value is required'); 15 | } 16 | var overridesThunk = function () { 17 | var overrides = {}; 18 | overrides[key] = valueThunk(); 19 | return overrides; 20 | }; 21 | return this.use(withOverrides, objectThunk, overridesThunk).extend('with override: ' + inspect(key)); 22 | }; 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | 32 | # Optional npm cache directory 33 | .npm 34 | 35 | # Optional REPL history 36 | .node_repl_history 37 | 38 | # Code coverage 39 | coverage 40 | 41 | # Only apps should have lockfiles 42 | npm-shrinkwrap.json 43 | package-lock.json 44 | yarn.lock 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jordan Harband 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 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | verbose: false 2 | instrumentation: 3 | root: . 4 | extensions: 5 | - .js 6 | - .jsx 7 | default-excludes: true 8 | excludes: [] 9 | variable: __coverage__ 10 | compact: true 11 | preserve-comments: false 12 | complete-copy: false 13 | save-baseline: false 14 | baseline-file: ./coverage/coverage-baseline.raw.json 15 | include-all-sources: false 16 | include-pid: false 17 | es-modules: false 18 | auto-wrap: false 19 | reporting: 20 | print: summary 21 | reports: 22 | - html 23 | dir: ./coverage 24 | summarizer: pkg 25 | report-config: {} 26 | watermarks: 27 | statements: [50, 80] 28 | functions: [50, 80] 29 | branches: [50, 80] 30 | lines: [50, 80] 31 | hooks: 32 | hook-run-in-context: false 33 | post-require-hook: null 34 | handle-sigint: false 35 | check: 36 | global: 37 | statements: 100 38 | lines: 100 39 | branches: 100 40 | functions: 100 41 | excludes: [] 42 | each: 43 | statements: 100 44 | lines: 100 45 | branches: 100 46 | functions: 100 47 | excludes: [] 48 | -------------------------------------------------------------------------------- /test/jest/withGlobal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var wrap = require('../..'); 5 | var thunk = function (v) { return function () { return v; }; }; 6 | 7 | describe('withGlobal plugin', function () { 8 | beforeAll(function () { 9 | global.foo = 42; 10 | global.bar = 100; 11 | global.baz = -1; 12 | }); 13 | 14 | afterAll(function () { 15 | delete global.foo; 16 | delete global.bar; 17 | delete global.baz; 18 | }); 19 | 20 | it('has globals set to initial values', function () { 21 | assert.equal(global.foo, 42); 22 | assert.equal(global.bar, 100); 23 | assert.equal(global.baz, -1); 24 | }); 25 | 26 | wrap().withGlobal('foo', thunk(123)).it('foo is 123', function () { 27 | assert.equal(global.foo, 123); 28 | assert.equal(global.bar, 100); 29 | assert.equal(global.baz, -1); 30 | }); 31 | 32 | wrap().withGlobal('foo', thunk(123)).withGlobal('bar', thunk(456)).describe('foo and bar', function () { 33 | it('has the right foo', function () { 34 | assert.equal(global.foo, 123); 35 | }); 36 | 37 | it('has the right bar', function () { 38 | assert.equal(global.bar, 456); 39 | }); 40 | 41 | it('has the right baz', function () { 42 | assert.equal(global.baz, -1); 43 | }); 44 | }); 45 | 46 | it('still has globals set to initial values', function () { 47 | assert.equal(global.foo, 42); 48 | assert.equal(global.bar, 100); 49 | assert.equal(global.baz, -1); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/jest/withOverride.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var has = require('has'); 5 | var wrap = require('../..'); 6 | var thunk = function (v) { return function () { return v; }; }; 7 | 8 | describe('withOverride plugin', function () { 9 | var obj = {}; 10 | beforeAll(function () { 11 | obj.foo = 'beforeAll foo'; 12 | obj.bar = 'beforeAll bar'; 13 | obj.baz = -1; 14 | obj.quux = 'quux'; 15 | }); 16 | 17 | it('has properties set to initial values', function () { 18 | assert.deepEqual(obj, { foo: 'beforeAll foo', bar: 'beforeAll bar', baz: -1, quux: 'quux' }); 19 | }); 20 | 21 | wrap().withOverride(thunk(obj), 'foo', thunk('after foo')) 22 | .test('foo is "after foo"', function () { 23 | assert.deepEqual(obj, { foo: 'after foo', bar: 'beforeAll bar', baz: -1, quux: 'quux' }); 24 | }); 25 | 26 | wrap() 27 | .withOverride(thunk(obj), 'foo', thunk('after foo')) 28 | .withOverride(thunk(obj), 'bar', thunk('after bar')) 29 | .describe('foo + bar', function () { 30 | it('is overridden as expected', function () { 31 | assert.deepEqual(obj, { foo: 'after foo', bar: 'after bar', baz: -1, quux: 'quux' }); 32 | }); 33 | }); 34 | 35 | it('still has properties set to initial values', function () { 36 | assert.deepEqual(obj, { foo: 'beforeAll foo', bar: 'beforeAll bar', baz: -1, quux: 'quux' }); 37 | }); 38 | 39 | it('lacks the key "absent"', function () { 40 | assert.equal(has(obj, 'absent'), false); 41 | }); 42 | 43 | wrap() 44 | .withOverride(thunk(obj), 'absent', thunk('yay')) 45 | .it('absent property is added', function () { 46 | assert.equal(has(obj, 'absent'), true); 47 | }); 48 | 49 | it('still lacks the key "absent"', function () { 50 | assert.equal(has(obj, 'absent'), false); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /.github/workflows/node-4+.yml: -------------------------------------------------------------------------------- 1 | name: 'Tests: node.js' 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | matrix: 7 | runs-on: ubuntu-latest 8 | outputs: 9 | latest: ${{ steps.set-matrix.outputs.requireds }} 10 | steps: 11 | - uses: ljharb/actions/node/matrix@main 12 | id: set-matrix 13 | with: 14 | versionsAsRoot: true 15 | type: 'majors' 16 | preset: '4 || 6 || 8 || 10 || 12 || 14 || >=16' 17 | 18 | latest: 19 | needs: [matrix] 20 | name: 'latest majors' 21 | runs-on: ubuntu-latest 22 | 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | node-version: ${{ fromJson(needs.matrix.outputs.latest) }} 27 | jest: 28 | - 27 29 | - 26 30 | - 25 31 | - 24 32 | - 23 33 | - 22 34 | - 21 35 | - 20 36 | - 19 37 | - 18 38 | exclude: 39 | - jest: 27 40 | node-version: 8 41 | - jest: 27 42 | node-version: 6 43 | - jest: 27 44 | node-version: 4 45 | - jest: 26 46 | node-version: 8 47 | - jest: 26 48 | node-version: 6 49 | - jest: 26 50 | node-version: 4 51 | - jest: 25 52 | node-version: 6 53 | - jest: 25 54 | node-version: 4 55 | - jest: 24 56 | node-version: 4 57 | - jest: 23 58 | node-version: 4 59 | - jest: 22 60 | node-version: 4 61 | 62 | steps: 63 | - uses: actions/checkout@v2 64 | - uses: ljharb/actions/node/install@main 65 | name: 'nvm install ${{ matrix.node-version }} && npm install' 66 | with: 67 | node-version: ${{ matrix.node-version }} 68 | skip-ls-check: true 69 | - run: npm run install:jest 70 | env: 71 | JEST: ${{ matrix.jest }} 72 | - run: npm prune 73 | - run: npm ls >/dev/null 74 | - run: npm run tests-only 75 | - uses: codecov/codecov-action@v1 76 | 77 | node: 78 | name: 'node 4+' 79 | needs: [latest] 80 | runs-on: ubuntu-latest 81 | steps: 82 | - run: 'echo tests completed' 83 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.7.0 / 2020-05-12 2 | ================== 3 | * [New] adds support for Jest ^26 4 | 5 | 1.6.0 / 2020-01-22 6 | ================== 7 | * [New] adds support for Jest ^25 8 | 9 | 1.5.0 / 2019-02-11 10 | ================== 11 | * [New] adds support for Jest ^24 12 | 13 | 1.4.0 / 2018-06-08 14 | ================== 15 | * [New] adds support for Jest ^23 16 | 17 | 1.3.1 / 2018-05-01 18 | ================= 19 | * [Fix] ensure that skip works inside plugins, and plugins with no changes but the mode work 20 | * [Deps] update `semver` 21 | * [Dev Deps] update `eslint`, `istanbul-lib-coverage`, `nsp`, `tape`, `eslint-plugin-import` 22 | 23 | 1.3.0 / 2018-01-04 24 | ================= 25 | * [New] adds support for Jest ^22 26 | * [Deps] update `function.prototype.name`, `is-primitive`, `object-inspect` 27 | * [Dev Deps] update `eslint`, `eslint-config-airbnb-base`, `eslint-plugin-import`, `nsp`, `rimraf` 28 | * Move repo to airbnb 29 | 30 | 1.2.0 / 2017-05-12 31 | ================= 32 | * [New] add `jest` `v21` support 33 | * [Deps] update `function-bind`, `function.prototype.name`, `object-inspect`, `semver` 34 | * [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `istanbul-lib-coverage`, `nsp`, `tape` 35 | * [Tests] make a matrix of jests 36 | * [Tests] only test major node versions; include `v8` 37 | * [Tests] use `nvm install-latest-npm` to ensure newer npms don’t break on older nodes 38 | 39 | 1.1.0 / 2017-05-12 40 | ================= 41 | * [New] Add jest 20 support (#9) 42 | * [Docs] Correct links/badges in the README (#8) 43 | * [Tests] on `node` `v7.10` 44 | * [Tests] Correct jest18/19 in package.json (#5) 45 | 46 | 1.0.2 / 2017-04-11 47 | ================= 48 | * [Fix] Fix descriptions when using multiple wrappers (#4) 49 | * [Fix] Stop reversing the afterEach hooks (#4) 50 | * [Fix] Update global beforeAll/afterAll hooks (#4) 51 | * [Fix] Remove .specify() (#4) 52 | * [Deps] update `object-inspect` 53 | * [Dev Deps] update `eslint`, `istanbul-lib-coverage` 54 | * [Tests] up to `node` `v7.9` 55 | * [Tests] Update core.js tests to work around bug in jest (#4) 56 | 57 | 1.0.1 / 2017-03-16 58 | ================= 59 | * [Fix] avoid exponentially adding outer wrappers (#3) 60 | * [Deps] lock `isarray` down to v1 only (v2 has a silly deprecation warning) 61 | * [Dev Deps] update `eslint`, `nsp`, `jest`, `rimraf` 62 | * [Tests] up to `node` `v7.7`, `v6.10`, `v4.8` 63 | 64 | 1.0.0 / 2017-02-18 65 | ================= 66 | * Initial release. 67 | -------------------------------------------------------------------------------- /withOverrides.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var entries = require('object.entries'); 4 | var has = require('has'); 5 | var isCallable = require('is-callable'); 6 | var isPrimitive = require('is-primitive'); 7 | var forEach = require('for-each'); 8 | var supportsDescriptors = require('define-properties').supportsDescriptors; 9 | 10 | module.exports = function withOverrides(objectThunk, overridesThunk) { 11 | if (!isCallable(objectThunk)) { 12 | throw new TypeError('a function that returns the object to override is required'); 13 | } 14 | if (!isCallable(overridesThunk)) { 15 | throw new TypeError('a function that returns the object from which to get overrides is required'); 16 | } 17 | var overridesData = []; 18 | return this.extend('with overrides', { 19 | beforeEach: function beforeEachWithOverrides() { 20 | var object = objectThunk(); 21 | if (isPrimitive(object)) { 22 | throw new TypeError('can not override on a non-object'); 23 | } 24 | var overrides = overridesThunk(); 25 | if (isPrimitive(overrides)) { 26 | throw new TypeError('can not override without an object from which to get overrides'); 27 | } 28 | var overridePairs = entries(overrides); 29 | var objectHadOwn = {}; 30 | var overridden = {}; 31 | forEach(overridePairs, function (entry) { 32 | var key = entry[0]; 33 | var value = entry[1]; 34 | if (has(object, key)) { 35 | objectHadOwn[key] = true; 36 | /* istanbul ignore else */ 37 | if (supportsDescriptors) { 38 | overridden[key] = Object.getOwnPropertyDescriptor(object, key); 39 | } else { 40 | overridden[key] = object[key]; 41 | } 42 | } 43 | /* istanbul ignore else */ 44 | if (supportsDescriptors) { 45 | Object.defineProperty(object, key, { 46 | configurable: true, 47 | enumerable: objectHadOwn[key] ? overridden[key].enumerable : true, 48 | value: value, 49 | writable: objectHadOwn[key] ? overridden[key].writable : true 50 | }); 51 | } else { 52 | object[key] = value; 53 | } 54 | }); 55 | overridesData.push({ 56 | objectHadOwn: objectHadOwn, 57 | overridden: overridden, 58 | object: object, 59 | overridePairs: overridePairs 60 | }); 61 | }, 62 | afterEach: function afterEachWithOverrides() { 63 | var data = overridesData.pop(); 64 | forEach(data.overridePairs, function (entry) { 65 | var key = entry[0]; 66 | if (data.objectHadOwn[key]) { 67 | /* istanbul ignore else */ 68 | if (supportsDescriptors) { 69 | Object.defineProperty(data.object, key, data.overridden[key]); 70 | } else { 71 | data.object[key] = data.overridden[key]; 72 | } 73 | } else { 74 | delete data.object[key]; 75 | } 76 | }); 77 | } 78 | }); 79 | }; 80 | -------------------------------------------------------------------------------- /test/jest/withOverrides.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var has = require('has'); 5 | var wrap = require('../..'); 6 | var thunk = function (v) { return function () { return v; }; }; 7 | var supportsDescriptors = require('define-properties').supportsDescriptors; 8 | 9 | describe('withOverrides plugin', function () { 10 | var obj = {}; 11 | beforeAll(function () { 12 | obj.foo = 'beforeAll foo'; 13 | obj.bar = 'beforeAll bar'; 14 | obj.baz = -1; 15 | obj.quux = 'quux'; 16 | }); 17 | 18 | it('has properties set to initial values', function () { 19 | assert.deepEqual(obj, { foo: 'beforeAll foo', bar: 'beforeAll bar', baz: -1, quux: 'quux' }); 20 | }); 21 | 22 | wrap().withOverrides(thunk(obj), thunk({ foo: 'after foo' })) 23 | .it('foo is "after foo"', function () { 24 | assert.deepEqual(obj, { foo: 'after foo', bar: 'beforeAll bar', baz: -1, quux: 'quux' }); 25 | }); 26 | 27 | wrap().withOverrides(thunk(obj), thunk({ foo: 'after foo' })) 28 | .withOverrides(thunk(obj), thunk({ bar: 'after bar', baz: 'after baz' })) 29 | .describe('foo + (bar, baz)', function () { 30 | it('is overridden as expected', function () { 31 | assert.deepEqual(obj, { foo: 'after foo', bar: 'after bar', baz: 'after baz', quux: 'quux' }); 32 | }); 33 | }); 34 | 35 | var holder = {}; 36 | wrap() 37 | .withOverrides(thunk(holder), function () { return { toMutate: {} }; }) 38 | .describe('mutations across multiple tests', function () { 39 | it('can be mutated in one test', function () { 40 | holder.toMutate.beforeAll = 'hi!'; 41 | assert.deepEqual(holder, { toMutate: { beforeAll: 'hi!' } }); 42 | }); 43 | 44 | it('is reset in another test', function () { 45 | assert.deepEqual(holder, { toMutate: {} }); 46 | }); 47 | 48 | it('is mutated afterwards, so test order doesn’t matter', function () { 49 | holder.toMutate.after = 'hi!'; 50 | assert.deepEqual(holder, { toMutate: { after: 'hi!' } }); 51 | }); 52 | }); 53 | 54 | it('still has properties set to initial values', function () { 55 | assert.deepEqual(obj, { foo: 'beforeAll foo', bar: 'beforeAll bar', baz: -1, quux: 'quux' }); 56 | }); 57 | 58 | it('lacks the key "absent"', function () { 59 | assert.equal(has(obj, 'absent'), false); 60 | }); 61 | 62 | wrap().withOverrides(thunk(obj), thunk({ absent: 'yay' })) 63 | .it('absent property is added', function () { 64 | assert.equal(has(obj, 'absent'), true); 65 | }); 66 | 67 | it('still lacks the key "absent"', function () { 68 | assert.equal(has(obj, 'absent'), false); 69 | }); 70 | 71 | var describeIfDescriptors = supportsDescriptors ? describe : describe.skip; 72 | describeIfDescriptors('when something is a getter', function () { 73 | var getter = Object.defineProperty({}, 'foo', { 74 | configurable: true, 75 | get: function () { return 42; }, 76 | enumerable: true 77 | }); 78 | 79 | wrap() 80 | .withOverrides(thunk(getter), thunk({ foo: 'bar' })) 81 | .it('overrides a getter', function () { 82 | assert.deepEqual(getter, { foo: 'bar' }); 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-wrap", 3 | "version": "1.7.0", 4 | "description": "Fluent pluggable interface for easily wrapping `describe` and `it` blocks in Jest tests.", 5 | "author": "Jordan Harband ", 6 | "contributors": [ 7 | { 8 | "name": "Jordan Harband", 9 | "email": "ljharb@gmail.com", 10 | "url": "http://ljharb.codes" 11 | }, 12 | { 13 | "name": "Joe Lencioni", 14 | "email": "joe.lencioni@gmail.com", 15 | "url": "https://twitter.com/lencioni" 16 | }, 17 | { 18 | "name": "Gary Borton", 19 | "email": "gdborton@gmail.com", 20 | "url": "https://twitter.com/garyborton" 21 | } 22 | ], 23 | "main": "index.js", 24 | "jest": { 25 | "collectCoverage": false, 26 | "roots": [ 27 | "/test" 28 | ], 29 | "testRegex": "test/.*\\.js$", 30 | "testURL": "http://localhost/" 31 | }, 32 | "scripts": { 33 | "prepublish": "safe-publish-latest", 34 | "pretest": "npm run lint", 35 | "test": "npm run tests-only", 36 | "posttest": "aud --production", 37 | "tests-only": "nyc npm run tests-all", 38 | "tests-all": "npm run tape && npm run jest:standard && npm run jest:only", 39 | "lint": "eslint --ext=.js,.mjs .", 40 | "tape": "tape test/tape", 41 | "jest:standard": "jest test/jest/*", 42 | "jest:only": "jest test/jest-only/*", 43 | "install:jest": "npm install --no-save jest@\"${JEST}\" && jest --version" 44 | }, 45 | "repository": { 46 | "type": "git", 47 | "url": "git+https://github.com/airbnb/jest-wrap.git" 48 | }, 49 | "keywords": [ 50 | "jest", 51 | "test", 52 | "javascript", 53 | "js", 54 | "chai", 55 | "before", 56 | "after", 57 | "beforeEach", 58 | "afterEach", 59 | "describe", 60 | "it", 61 | "wrap", 62 | "around", 63 | "around_filter", 64 | "mock", 65 | "stub" 66 | ], 67 | "license": "MIT", 68 | "bugs": { 69 | "url": "https://github.com/airbnb/jest-wrap/issues" 70 | }, 71 | "homepage": "https://github.com/airbnb/jest-wrap#readme", 72 | "dependencies": { 73 | "define-properties": "^1.1.2", 74 | "for-each": "^0.3.3", 75 | "function-bind": "^1.1.1", 76 | "function.prototype.name": "^1.1.0", 77 | "has": "^1.0.3", 78 | "is-callable": "^1.1.3", 79 | "is-primitive": "^3.0.0", 80 | "is-string": "^1.0.4", 81 | "is-symbol": "^1.0.1", 82 | "isarray": "^1.0.0 || ^2.0.0", 83 | "object-inspect": "^1.6.0", 84 | "object.entries": "^1.0.4" 85 | }, 86 | "peerDependencies": { 87 | "jest": "^18 || ^19 || ^20.0.1 || ^21 || ^22 || ^23 || ^24 || ^25 || ^26 || ^27.2" 88 | }, 89 | "devDependencies": { 90 | "aud": "^1.1.5", 91 | "eslint": "^4.19.1", 92 | "eslint-config-airbnb-base": "^12.1.0", 93 | "eslint-plugin-import": "^2.11.0", 94 | "jest": "^18 || ^19 || ^20.0.1 || ^21 || ^22 || ^23 || ^24 || ^25 || ^26 || ^27.2", 95 | "nyc": "^12.0.2", 96 | "rimraf": "^2.6.2", 97 | "safe-publish-latest": "^1.1.1", 98 | "semver": "^6.3.0", 99 | "tape": "^4.9.0" 100 | }, 101 | "engines": { 102 | "node": ">= 0.4" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jest-wrap [![Version Badge][2]][1] 2 | 3 | [![Build Status][3]][4] 4 | [![dependency status][5]][6] 5 | [![dev dependency status][7]][8] 6 | [![License][license-image]][license-url] 7 | [![Downloads][downloads-image]][downloads-url] 8 | 9 | [![npm badge][11]][1] 10 | 11 | Fluent pluggable interface for easily wrapping `describe`, `it`, and `test` blocks in Jest tests. 12 | 13 | ## Example 14 | 15 | ```js 16 | var wrap = require('jest-wrap'); 17 | var expect = require('chai').expect; 18 | 19 | var mockWindow = { 20 | location: { 21 | href: 'test/url' 22 | } 23 | }; 24 | wrap().withGlobal('window', () => mockWindow).describe('mocked window', function () { 25 | it('is mocked', function () { 26 | expect(window).to.equal(mockWindow); 27 | }); 28 | 29 | it('has the right URL', function () { 30 | expect(window.location.href).to.equal('test/url'); 31 | }); 32 | }); 33 | 34 | var obj = { a: 1 }; 35 | wrap().withOverrides(() => obj, () => ({ a: 2, b: 3 })).describe('overridden object keys', function () { 36 | it('has "b"', function () { 37 | expect(obj.b).to.equal(3); 38 | }); 39 | 40 | it('has overridden "a"', function () { 41 | expect(obj.a).to.equal(2); 42 | }); 43 | }); 44 | 45 | wrap().withOverride(() => obj, 'a', () => 4).skip().describe('this test is skipped', function () { 46 | it('also supports .only()!', function () { 47 | expect(true).to.equal(false); // skipped 48 | }); 49 | }); 50 | ``` 51 | 52 | ## Plugins 53 | A `jest-wrap` **plugin** is a named function that returns a JestWrapper instance or a descriptor object. 54 | - A plugin’s function `name` must begin with the string “with”. 55 | - Plugins can be globally registered, or `.use`d ad-hoc. 56 | - `.use` requires a plugin function as its first argument; further arguments are passed through to the plugin. 57 | - `.extend` requires a non-empty description string, and a descriptor object which may contain a value that is a function, or an array of functions, whose keys correspond to any or all of the supported jest methods. 58 | - Globally registered plugins, `.use` calls, and `.extend` calls can be chained, stored, and reused - each link in the chain creates a new instance of a JestWrapper. 59 | 60 | - A descriptor object may contain any or all of these 5 keys: 61 | - a `description` string, for use in “describe” and/or “it” (this is required when returning an object) 62 | - `beforeEach`: a function, or array of functions, for use in a `jest` `beforeEach` function 63 | - `afterEach`: a function, or array of functions, for use in a `jest` `afterEach` function 64 | - `beforeAll`: a function, or array of functions, for use in a `jest` `beforeAll` function 65 | - `afterAll`: a function, or array of functions, for use in a `jest` `afterAll` function 66 | 67 | The most common approach will be for a plugin function to return `this.extend(description, descriptor)`. 68 | 69 | A plugin function must have a `name` that starts with “with”, and will be invoked with a receiver (”this” value) of a JestWrapper instance. 70 | 71 | To register a plugin, call the `register` function on `jest-wrap` with the plugin function. This should not be done in a reusable module. 72 | 73 | ```js 74 | module.exports = function withFoo(any, args, you, want) { 75 | return this.extend('with some foo stuff', { 76 | beforeEach: function () { 77 | // setup ran before each test 78 | }, 79 | afterEach: [ 80 | function () { 81 | // teardown ran after each test 82 | }, 83 | function () { 84 | // more teardown 85 | } 86 | ], 87 | beforeAll: function () { 88 | // setup ran once before all tests 89 | }, 90 | afterAll: function () { 91 | // teardown ran once after all tests 92 | } 93 | }); 94 | }; 95 | ``` 96 | 97 | ## Usage 98 | ```js 99 | var wrap = require('jest-wrap'); 100 | wrap.register(require('jest-wrap-with-foo')); 101 | 102 | wrap().withFoo().describe… 103 | ``` 104 | 105 | ## skip/only 106 | Although jest has `describe.skip`, `describe.only`, `it.skip`, `it.only`, `test.skip`, and `test.only`, it is not possible to implement these in jest-wrap without using ES5 property accessors. Since this project supports ES3, we decided to use `.skip().describe` etc rather than forfeit the ability to have skip/only. 107 | 108 | ## Tests 109 | Simply clone the repo, `npm install`, and run `npm test` 110 | 111 | [1]: https://npmjs.org/package/jest-wrap 112 | [2]: http://versionbadg.es/airbnb/jest-wrap.svg 113 | [3]: https://travis-ci.org/airbnb/jest-wrap.svg 114 | [4]: https://travis-ci.org/airbnb/jest-wrap 115 | [5]: https://david-dm.org/airbnb/jest-wrap.svg 116 | [6]: https://david-dm.org/airbnb/jest-wrap 117 | [7]: https://david-dm.org/airbnb/jest-wrap/dev-status.svg 118 | [8]: https://david-dm.org/airbnb/jest-wrap#info=devDependencies 119 | [11]: https://nodei.co/npm/jest-wrap.png?downloads=true&stars=true 120 | [license-image]: http://img.shields.io/npm/l/jest-wrap.svg 121 | [license-url]: LICENSE 122 | [downloads-image]: http://img.shields.io/npm/dm/jest-wrap.svg 123 | [downloads-url]: http://npm-stat.com/charts.html?package=jest-wrap 124 | -------------------------------------------------------------------------------- /test/jest/core.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var jestVersion = require('jest/package.json').version; 5 | var semver = require('semver'); 6 | var wrap = require('../../'); 7 | 8 | describe('core JestWrapper semantics', function () { 9 | describe('when there are no transformations', function () { 10 | it('throws when there are no transformations', function () { 11 | assert['throws'](function () { wrap().describe('foo', function () {}); }, RangeError); 12 | assert['throws'](function () { wrap().it('foo', function () {}); }, RangeError); 13 | assert['throws'](function () { wrap().test('foo', function () {}); }, RangeError); 14 | }); 15 | 16 | (semver.satisfies(jestVersion, '< 27') ? it : it.skip)('describe does not throw when the mode is "skip"', function () { 17 | assert.doesNotThrow(function () { wrap().skip().describe('foo', function () {}); }); 18 | }); 19 | 20 | (semver.satisfies(jestVersion, '< 21') ? it : it.skip)('it/test do not throw when the mode is "skip"', function () { 21 | assert.doesNotThrow(function () { wrap().skip().it('foo', function () {}); }); 22 | assert.doesNotThrow(function () { wrap().skip().test('foo', function () {}); }); 23 | }); 24 | }); 25 | 26 | var withNothing = function withNothing() { 27 | return { description: 'i am pointless' }; 28 | }; 29 | 30 | var testingCount = 0; 31 | var withTesting = function withTesting() { 32 | return this.extend('(with Testing)', { 33 | beforeEach: function withTestingBeforeEach() { 34 | testingCount += 1; 35 | } 36 | }); 37 | }; 38 | 39 | var withFancyNoop = function withFancyNoop() { 40 | return this.extend('(withFancyNoop)', {}); 41 | }; 42 | 43 | var withSkip = function withSkip() { 44 | return this.skip().extend('i am skipped', {}); 45 | }; 46 | 47 | wrap() 48 | .use(withSkip) 49 | .it('skips a test when the plugin skips it', function () { 50 | throw new SyntaxError('this should never run'); 51 | }); 52 | 53 | wrap() 54 | .use(withTesting) 55 | .use(withFancyNoop) 56 | .describe('with multiple plugins', function () { 57 | it('calls the plugin\'s hooks an appropriate number of times', function () { 58 | assert.equal(testingCount, 1); 59 | }); 60 | }); 61 | 62 | describe('#use()', function () { 63 | var flag = false; 64 | var withDescriptor = function withDescriptor() { 65 | return { 66 | description: 'i am a descriptor', 67 | beforeEach: function () { flag = true; }, 68 | afterEach: function () { flag = false; } 69 | }; 70 | }; 71 | 72 | wrap().use(withDescriptor).describe('with a plugin that returns a descriptor', function () { 73 | it('works', function () { 74 | assert.equal(flag, true); 75 | }); 76 | }); 77 | 78 | wrap().use(withNothing).it('works with a plugin that returns a descriptor with only a description', function () { 79 | assert.equal(true, true); // oh yeah, TESTING 80 | }); 81 | }); 82 | 83 | describe('#skip()', function () { 84 | wrap().use(withNothing).skip().it('skipped an it!', function () { 85 | assert.equal(true, false); // boom 86 | }); 87 | 88 | wrap().use(withNothing).skip().describe('skipped a describe!', function () { 89 | it('fails if not skipped', function () { 90 | assert.equal(true, false); // boom 91 | }); 92 | }); 93 | 94 | wrap().use(withNothing).skip().test('skipped a test!', function () { 95 | it('fails if not skipped', function () { 96 | assert.equal(true, false); // boom 97 | }); 98 | }); 99 | }); 100 | 101 | /** 102 | * Temporarily replace describe so that we can assert on the passed params. 103 | */ 104 | var originalDescribe = global.describe; 105 | var passedDescription; 106 | global.describe = function (description) { 107 | passedDescription = description; 108 | global.describe = originalDescribe; // revert to mocha's describe. 109 | return originalDescribe.apply(this, arguments); 110 | }; 111 | 112 | wrap() 113 | .use(withFancyNoop) 114 | .use(withTesting) 115 | .describe('wrapped descriptions', function () { 116 | it('should have the proper description', function () { 117 | assert.equal(passedDescription, 'wrapped: (withFancyNoop); (with Testing):'); 118 | }); 119 | }); 120 | 121 | var calls = []; 122 | describe('ordering of before/afters with multiple plugins', function () { 123 | var testsRun = 0; 124 | wrap().extend('first', { 125 | beforeAll: function () { calls.push('>:beforeAll'); }, 126 | beforeEach: function () { calls.push('>:beforeEach'); }, 127 | afterAll: function () { calls.push('>:afterAll'); }, 128 | afterEach: function () { calls.push('>:afterEach'); } 129 | }).extend('second', { 130 | beforeAll: function () { calls.push('>>:beforeAll'); }, 131 | beforeEach: function () { calls.push('>>:beforeEach'); }, 132 | afterAll: function () { calls.push('>>:afterAll'); }, 133 | afterEach: function () { calls.push('>>:afterEach'); } 134 | }).describe('with method tracking', function () { 135 | it('is one test', function () { 136 | testsRun += 1; 137 | calls.push('>>>:test'); 138 | }); 139 | 140 | it('is another test', function () { 141 | testsRun += 1; 142 | calls.push('>>>:test'); 143 | }); 144 | }); 145 | 146 | it('runs the hooks/tests in the correct order', function () { 147 | if (testsRun !== 2) { throw new Error('This test cannot be run in isolation.'); } 148 | var expectedCalls = [].concat( 149 | '>:beforeAll', 150 | '>>:beforeAll', 151 | 152 | '>:beforeEach', 153 | '>>:beforeEach', 154 | '>>>:test', 155 | semver.satisfies(jestVersion, '>= 27') ? [ 156 | '>:afterEach', 157 | '>>:afterEach' 158 | ] : [ 159 | '>>:afterEach', 160 | '>:afterEach' 161 | ], 162 | 163 | '>:beforeEach', 164 | '>>:beforeEach', 165 | '>>>:test', 166 | semver.satisfies(jestVersion, '>= 27') ? [ 167 | '>:afterEach', 168 | '>>:afterEach', 169 | '>:afterAll', 170 | '>>:afterAll' 171 | ] : [ 172 | '>>:afterEach', 173 | '>:afterEach', 174 | '>>:afterAll', 175 | '>:afterAll' 176 | ] 177 | ); 178 | 179 | expect(calls).toEqual(expectedCalls); 180 | }); 181 | }); 182 | }); 183 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* globals WeakMap */ 4 | 5 | var isCallable = require('is-callable'); 6 | var isString = require('is-string'); 7 | var has = require('has'); 8 | var forEach = require('for-each'); 9 | var isArray = require('isarray'); 10 | var functionName = require('function.prototype.name'); 11 | var inspect = require('object-inspect'); 12 | var semver = require('semver'); 13 | var jestVersion = require('jest').getVersion(); 14 | 15 | var checkWithName = require('./helpers/checkWithName'); 16 | 17 | var withOverrides = require('./withOverrides'); 18 | var withOverride = require('./withOverride'); 19 | var withGlobal = require('./withGlobal'); 20 | 21 | var hasPrivacy = typeof WeakMap === 'function'; 22 | var wrapperMap = hasPrivacy ? new WeakMap() : /* istanbul ignore next */ null; 23 | var modeMap = hasPrivacy ? new WeakMap() : /* istanbul ignore next */ null; 24 | 25 | var MODE_ALL = 'all'; 26 | var MODE_SKIP = 'skip'; 27 | var MODE_ONLY = 'only'; 28 | 29 | var beforeMethods = ['beforeAll', 'beforeEach']; 30 | var afterMethods = ['afterAll', 'afterEach']; 31 | var supportedMethods = [].concat(beforeMethods, afterMethods); 32 | 33 | /** 34 | * There is a bug in Jest 18/19 that processes the afterAll hooks in the wrong 35 | * order. This bit of logic is meant to understand which method Jest is using 36 | * for moving through the list of afterAll hooks, and supply them in the order 37 | * that gets them applied in the order we want. 38 | */ 39 | var needsAfterAllReversal = semver.satisfies(jestVersion, '< 20'); 40 | 41 | var JestWrapper; 42 | 43 | var checkThis = function requireJestWrapper(instance) { 44 | if (!instance || typeof instance !== 'object' || !(instance instanceof JestWrapper)) { 45 | throw new TypeError(inspect(instance) + ' must be a JestWrapper'); 46 | } 47 | return instance; 48 | }; 49 | 50 | var setThisWrappers = function (instance, value) { 51 | checkThis(instance); 52 | /* istanbul ignore else */ 53 | if (hasPrivacy) { 54 | wrapperMap.set(instance, value); 55 | } else { 56 | instance.wrappers = value; // eslint-disable-line no-param-reassign 57 | } 58 | return instance; 59 | }; 60 | 61 | var getThisWrappers = function (instance) { 62 | checkThis(instance); 63 | return hasPrivacy ? wrapperMap.get(instance) : /* istanbul ignore next */ instance.wrappers; 64 | }; 65 | 66 | var setThisMode = function (instance, mode) { 67 | checkThis(instance); 68 | /* istanbul ignore else */ 69 | if (hasPrivacy) { 70 | modeMap.set(instance, mode); 71 | } else { 72 | instance.mode = mode; // eslint-disable-line no-param-reassign 73 | } 74 | return instance; 75 | }; 76 | 77 | var getThisMode = function (instance) { 78 | checkThis(instance); 79 | return hasPrivacy ? modeMap.get(instance) : /* istanbul ignore next */ instance.mode; 80 | }; 81 | 82 | JestWrapper = function JestWrapper() { // eslint-disable-line no-shadow 83 | setThisWrappers(this, []); 84 | setThisMode(this, MODE_ALL); 85 | }; 86 | 87 | var createWithWrappers = function (wrappers) { 88 | return setThisWrappers(new JestWrapper(), wrappers); 89 | }; 90 | 91 | var concatThis = function (instance, toConcat) { 92 | var thisWrappers = getThisWrappers(instance); 93 | var thisMode = getThisMode(instance); 94 | return setThisMode(createWithWrappers(thisWrappers.concat(toConcat || [])), thisMode); 95 | }; 96 | 97 | var flattenToDescriptors = function flattenToDescriptors(wrappers) { 98 | if (wrappers.length === 0) { return []; } 99 | 100 | var descriptors = []; 101 | forEach(wrappers, function (wrapper) { 102 | var subWrappers = wrapper instanceof JestWrapper ? getThisWrappers(wrapper) : wrapper; 103 | if (Array.isArray(subWrappers)) { 104 | descriptors.push.apply(descriptors, flattenToDescriptors(subWrappers)); 105 | } else { 106 | descriptors.push(subWrappers); 107 | } 108 | }); 109 | return descriptors; 110 | }; 111 | 112 | var applyMethods = function applyMethods(methodsToApply, descriptors) { 113 | forEach(descriptors, function (methods) { 114 | forEach(methodsToApply, function (method) { 115 | var functions = methods[method]; 116 | if (functions) { 117 | forEach(functions, function (func) { 118 | global[method](func); 119 | }); 120 | } 121 | }); 122 | }); 123 | }; 124 | 125 | var createAssertion = function createAssertion(type, message, wrappers, block, mode) { 126 | var descriptors = flattenToDescriptors(wrappers); 127 | if (descriptors.length === 0 && mode === MODE_ALL) { 128 | throw new RangeError(inspect(type) + ' called with no wrappers defined'); 129 | } 130 | 131 | var describeMsgs = []; 132 | forEach(descriptors, function (descriptor) { 133 | if (descriptor.description) { 134 | describeMsgs.push(descriptor.description); 135 | } 136 | }); 137 | 138 | var describeMsg = 'wrapped: ' + describeMsgs.join('; ') + ':'; 139 | var describeMethod = global.describe; 140 | if (mode === MODE_SKIP) { 141 | describeMethod = global.describe.skip; 142 | } else if (mode === MODE_ONLY) { 143 | describeMethod = global.describe.only; 144 | } 145 | 146 | describeMethod(describeMsg, function () { 147 | applyMethods(beforeMethods, descriptors); 148 | global[type](message, block); 149 | 150 | // See comment at top of file. 151 | if (needsAfterAllReversal) { 152 | applyMethods(['afterEach'], descriptors); 153 | applyMethods(['afterAll'], descriptors.reverse()); 154 | } else { 155 | applyMethods(afterMethods, descriptors); 156 | } 157 | }); 158 | }; 159 | 160 | JestWrapper.prototype.skip = function skip() { 161 | return setThisMode(concatThis(this), MODE_SKIP); 162 | }; 163 | 164 | JestWrapper.prototype.only = function only() { 165 | return setThisMode(concatThis(this), MODE_ONLY); 166 | }; 167 | 168 | JestWrapper.prototype.it = function it(msg, fn) { 169 | var wrappers = getThisWrappers(checkThis(this)); 170 | var mode = getThisMode(this); 171 | createAssertion('it', msg, wrappers, fn, mode); 172 | }; 173 | JestWrapper.prototype.it.skip = function skip() { 174 | throw new SyntaxError('jest-wrap requires `.skip().it` rather than `it.skip`'); 175 | }; 176 | JestWrapper.prototype.it.only = function only() { 177 | throw new SyntaxError('jest-wrap requires `.only().it` rather than `it.only`'); 178 | }; 179 | 180 | JestWrapper.prototype.test = function test(msg, fn) { 181 | var wrappers = getThisWrappers(checkThis(this)); 182 | var mode = getThisMode(this); 183 | createAssertion('test', msg, wrappers, fn, mode); 184 | }; 185 | JestWrapper.prototype.test.skip = function skip() { 186 | throw new SyntaxError('jest-wrap requires `.skip().test` rather than `test.skip`'); 187 | }; 188 | JestWrapper.prototype.test.only = function only() { 189 | throw new SyntaxError('jest-wrap requires `.only().test` rather than `test.only`'); 190 | }; 191 | 192 | JestWrapper.prototype.describe = function describe(msg, fn) { 193 | var wrappers = getThisWrappers(checkThis(this)); 194 | var mode = getThisMode(this); 195 | createAssertion('describe', msg, wrappers, fn, mode); 196 | }; 197 | JestWrapper.prototype.describe.skip = function skip() { 198 | throw new SyntaxError('jest-wrap requires `.skip().describe` rather than `describe.skip`'); 199 | }; 200 | JestWrapper.prototype.describe.only = function only() { 201 | throw new SyntaxError('jest-wrap requires `.only().describe` rather than `describe.only`'); 202 | }; 203 | 204 | var wrap = function wrap() { return new JestWrapper(); }; 205 | 206 | var isWithNameAvailable = function (name) { 207 | checkWithName(name); 208 | return !has(JestWrapper.prototype, name) || !isCallable(JestWrapper.prototype[name]); 209 | }; 210 | 211 | wrap.supportedMethods = isCallable(Object.freeze) 212 | ? Object.freeze(supportedMethods) 213 | : /* istanbul ignore next */ supportedMethods.slice(); 214 | 215 | JestWrapper.prototype.extend = function extend(description, descriptor) { 216 | checkThis(this); 217 | if (!isString(description) || description.length === 0) { 218 | throw new TypeError('a non-empty description string is required'); 219 | } 220 | var newWrappers = []; 221 | if (descriptor) { 222 | forEach(supportedMethods, function (methodName) { 223 | if (methodName in descriptor) { 224 | if (!isArray(descriptor[methodName])) { 225 | // eslint-disable-next-line no-param-reassign 226 | descriptor[methodName] = [descriptor[methodName]]; 227 | } 228 | forEach(descriptor[methodName], function (method) { 229 | if (!isCallable(method)) { 230 | throw new TypeError('wrapper method "' + method + '" must be a function, or array of functions, if present'); 231 | } 232 | }); 233 | } 234 | }); 235 | descriptor.description = description; // eslint-disable-line no-param-reassign 236 | newWrappers = [createWithWrappers([descriptor])]; 237 | } 238 | return concatThis(this, newWrappers); 239 | }; 240 | 241 | JestWrapper.prototype.use = function use(plugin) { 242 | checkThis(this); 243 | if (!isCallable(plugin)) { 244 | throw new TypeError('plugin must be a function'); 245 | } 246 | var withName = functionName(plugin); 247 | checkWithName(withName); 248 | 249 | var extraArguments = Array.prototype.slice.call(arguments, 1); 250 | var descriptorOrInstance = plugin.apply(this, extraArguments) || {}; 251 | 252 | var instance = descriptorOrInstance; 253 | if (!(descriptorOrInstance instanceof JestWrapper)) { 254 | instance = wrap().extend(descriptorOrInstance.description, descriptorOrInstance); 255 | } 256 | 257 | var thisMode = getThisMode(instance); 258 | return setThisMode(setThisWrappers(new JestWrapper(), [instance]), thisMode); 259 | }; 260 | 261 | wrap.register = function register(plugin) { 262 | var withName = functionName(plugin); 263 | checkWithName(withName); 264 | if (!isWithNameAvailable(withName)) { 265 | // already registered 266 | return; 267 | } 268 | JestWrapper.prototype[withName] = function wrapper() { 269 | return this.use.apply(this, [plugin].concat(Array.prototype.slice.call(arguments))); 270 | }; 271 | }; 272 | 273 | wrap.unregister = function unregister(pluginOrWithName) { 274 | var withName = isCallable(pluginOrWithName) ? functionName(pluginOrWithName) : pluginOrWithName; 275 | checkWithName(withName); 276 | if (isWithNameAvailable(withName)) { 277 | throw new RangeError('error: plugin "' + withName + '" is not registered.'); 278 | } 279 | delete JestWrapper.prototype[withName]; 280 | }; 281 | 282 | wrap.register(withOverrides); 283 | wrap.register(withOverride); 284 | wrap.register(withGlobal); 285 | 286 | module.exports = wrap; 287 | -------------------------------------------------------------------------------- /test/tape/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* globals WeakMap */ 4 | 5 | var test = require('tape'); 6 | 7 | var wrap = require('../../'); 8 | var withGlobal = require('../../withGlobal'); 9 | var withOverrides = require('../../withOverrides'); 10 | var withOverride = require('../../withOverride'); 11 | 12 | var hasPrivacy = typeof WeakMap === 'function'; 13 | 14 | var setup = function setup() { 15 | var msgFn = function (msg, fn) { fn(); }; 16 | global.describe = msgFn; 17 | global.test = msgFn; 18 | global.it = msgFn; 19 | var runFn = function (fn) { fn(); }; 20 | global.beforeAll = runFn; 21 | global.beforeEach = runFn; 22 | global.afterAll = runFn; 23 | global.afterEach = runFn; 24 | }; 25 | var teardown = function teardown() { 26 | delete global.describe; 27 | delete global.test; 28 | delete global.it; 29 | delete global.before; 30 | delete global.beforeEach; 31 | delete global.after; 32 | delete global.afterEach; 33 | }; 34 | 35 | test('jest-wrap', function (t) { 36 | setup(); 37 | t.on('end', teardown); 38 | 39 | t.test('no transformations', function (st) { 40 | st['throws'](function () { wrap().describe('foo', function () {}); }, RangeError, 'throws when there are no transformations'); 41 | st['throws'](function () { wrap().test('foo', function () {}); }, RangeError, 'throws when there are no transformations'); 42 | st['throws'](function () { wrap().it('foo', function () {}); }, RangeError, 'throws when there are no transformations'); 43 | st.end(); 44 | }); 45 | 46 | t.test('{describe,test,it}.{skip,only} throw', function (st) { 47 | st['throws'](function () { wrap().describe.skip('skip'); }, SyntaxError, 'describe.skip throws'); 48 | st['throws'](function () { wrap().describe.only('only'); }, SyntaxError, 'describe.only throws'); 49 | st['throws'](function () { wrap().test.skip('skip'); }, SyntaxError, 'test.skip throws'); 50 | st['throws'](function () { wrap().test.only('only'); }, SyntaxError, 'test.only throws'); 51 | st['throws'](function () { wrap().it.skip('skip'); }, SyntaxError, 'it.skip throws'); 52 | st['throws'](function () { wrap().it.only('only'); }, SyntaxError, 'it.only throws'); 53 | st.end(); 54 | }); 55 | 56 | t.test('withOverrides deferred exceptions', function (st) { 57 | st['throws'](function () { 58 | wrap().withOverrides(function () { return null; }, function () { return {}; }).describe('msg', function () {}); 59 | }, TypeError, 'requires objectThunk to return an object'); 60 | st['throws'](function () { 61 | wrap().withOverrides(function () { return {}; }, function () { return null; }).describe('msg', function () {}); 62 | }, TypeError, 'requires overridesThunk to return an object'); 63 | st.end(); 64 | }); 65 | 66 | t.test('.supportedMethods', function (st) { 67 | st.equal(Array.isArray(wrap.supportedMethods), true, 'is an array'); 68 | st.deepEqual(wrap.supportedMethods, ['beforeAll', 'beforeEach', 'afterAll', 'afterEach'], 'has the expected methods'); 69 | st.end(); 70 | }); 71 | 72 | t.test('.register()', function (st) { 73 | st.equal(typeof wrap.register, 'function', 'is a function'); 74 | 75 | st.test('throws on an invalid plugin name', function (st2) { 76 | st2['throws'](function () { wrap.register(); }, TypeError, 'throws on a non-string plugin name'); 77 | st2['throws'](function () { wrap.register(''); }, TypeError, 'throws on an empty string plugin name'); 78 | st2['throws'](function () { wrap.register('foo'); }, TypeError, 'throws on a non-prefixed plugin name'); 79 | st2.end(); 80 | }); 81 | 82 | st.end(); 83 | }); 84 | 85 | t.test('.unregister()', function (st) { 86 | st.equal(typeof wrap.unregister, 'function', 'is a function'); 87 | 88 | st.test('throws on an invalid plugin name', function (st2) { 89 | st2['throws'](function () { wrap.unregister(); }, TypeError, 'throws on a non-string plugin name'); 90 | st2['throws'](function () { wrap.unregister(''); }, TypeError, 'throws on an empty string plugin name'); 91 | st2['throws'](function () { wrap.unregister('foo'); }, TypeError, 'throws on a non-prefixed plugin name'); 92 | st2.end(); 93 | }); 94 | 95 | st['throws'](function () { wrap.unregister('withNope'); }, RangeError, 'throws on an unregistered plugin'); 96 | 97 | st.end(); 98 | }); 99 | 100 | var plugin = function withFoo() { return this.extend('…'); }; 101 | t.test('registration/unregistration', function (st) { 102 | var instance = wrap(); 103 | st.equal(typeof instance.withFoo, 'undefined', 'withFoo is not registered'); 104 | wrap.register(plugin); 105 | st.equal(typeof instance.withFoo, 'function', 'withFoo is registered'); 106 | wrap.unregister('withFoo'); 107 | st.equal(typeof instance.withFoo, 'undefined', 'withFoo is unregistered by name'); 108 | wrap.register(plugin); 109 | st.equal(typeof instance.withFoo, 'function', 'withFoo is again registered'); 110 | wrap.unregister(plugin); 111 | st.equal(typeof instance.withFoo, 'undefined', 'withFoo is unregistered by function'); 112 | st.end(); 113 | }); 114 | 115 | t.test('duplicate registration', function (st) { 116 | var instance = wrap(); 117 | wrap.register(plugin); 118 | var original = instance.withFoo; 119 | wrap.register(plugin); 120 | st.equal(original, instance.withFoo, 'duplicate "register" is a noop'); 121 | st.end(); 122 | }); 123 | 124 | t.end(); 125 | }); 126 | 127 | test('JestWrapper', function (t) { 128 | t.notEqual(wrap(), wrap(), 'wrap() returns different instances'); 129 | 130 | var instance = wrap(); 131 | var described = instance.extend('described!'); 132 | t.notEqual(instance, described, 'sanity check: .extend() returns a new instance'); 133 | 134 | t.test('hidden instance properties', { skip: !hasPrivacy }, function (st) { 135 | st.deepEqual(Object.keys(instance), [], 'has no own keys'); 136 | st.deepEqual(Object.keys(described), [], 'has no own keys'); 137 | 138 | st.end(); 139 | }); 140 | 141 | t.test('visible instance properties', { skip: hasPrivacy }, function (st) { 142 | st.deepEqual(Object.keys(instance), ['wrappers', 'mode'], 'has "wrappers" and "mode" key'); 143 | st.deepEqual(Object.keys(described), ['wrappers', 'mode', 'description'], 'has "wrappers", "mode", and "description" keys'); 144 | 145 | st.end(); 146 | }); 147 | 148 | t.test('#use()', function (st) { 149 | st.test('borrowing on a non-wrapper', function (sst) { 150 | sst['throws'](function () { instance.use.call({}, function withFoo() {}); }, TypeError, 'throws when receiver is not JestWrapper'); 151 | sst.end(); 152 | }); 153 | 154 | st.test('non-functions', function (sst) { 155 | sst['throws'](function () { instance.use(); }, TypeError, 'throws with undefined'); 156 | sst['throws'](function () { instance.use(null); }, TypeError, 'throws with null'); 157 | sst['throws'](function () { instance.use(true); }, TypeError, 'throws with true'); 158 | sst['throws'](function () { instance.use('foo'); }, TypeError, 'throws with string'); 159 | sst['throws'](function () { instance.use(/a/g); }, TypeError, 'throws with regex'); 160 | sst['throws'](function () { instance.use([]); }, TypeError, 'throws with array'); 161 | sst['throws'](function () { instance.use({}); }, TypeError, 'throws with object'); 162 | sst.end(); 163 | }); 164 | 165 | st.test('functions without the correct name', function (sst) { 166 | var anon = function () {}; 167 | sst['throws'](function () { instance.use(anon); }, TypeError, 'throws with anonymous function'); 168 | var badPlugin = function wrongPrefix() {}; 169 | sst['throws'](function () { instance.use(badPlugin); }, TypeError, 'throws with badly named function'); 170 | sst.end(); 171 | }); 172 | 173 | st.test('with a plugin', function (sst) { 174 | sst.test('plugins not returning a description', function (s2t) { 175 | s2t['throws']( 176 | function () { instance.use(function withFoo() {}); }, 177 | TypeError, 178 | 'throws when plugin returns undefined' 179 | ); 180 | s2t['throws']( 181 | function () { instance.use(function withFoo() { return {}; }); }, 182 | TypeError, 183 | 'throws when plugin returns no description' 184 | ); 185 | s2t['throws']( 186 | function () { instance.use(function withFoo() { return { description: true }; }); }, 187 | TypeError, 188 | 'throws when plugin returns nonstring description' 189 | ); 190 | s2t['throws']( 191 | function () { instance.use(function withFoo() { return { description: '' }; }); }, 192 | TypeError, 193 | 'throws when plugin returns empty description' 194 | ); 195 | s2t.end(); 196 | }); 197 | 198 | sst.test('receiver and proxied arguments', function (s2t) { 199 | s2t.plan(7); 200 | var plugin = function withFoo(a, b, c, d) { 201 | s2t.equal(arguments.length, 3, '3 args passed'); 202 | s2t.equal(a, 1, 'a arg is 1'); 203 | s2t.equal(b, 2, 'b arg is 1'); 204 | s2t.equal(c, 3, 'c arg is 1'); 205 | s2t.equal(d, undefined, 'd arg is undefined'); 206 | s2t.equal(instance, this, 'plugin receiver is instance'); 207 | return { description: 'something' }; 208 | }; 209 | var withPlugin = instance.use(plugin, 1, 2, 3); 210 | s2t.notEqual(instance, withPlugin, 'sanity check: "use" returns a new instance'); 211 | }); 212 | 213 | sst.test('plugin that calls .extend()', function (s2t) { 214 | var plugin = function withFoo() { 215 | return this.extend('description'); 216 | }; 217 | var withPlugin = instance.use(plugin); 218 | s2t.notEqual(instance, withPlugin, 'sanity check: "use" returns a new instance'); 219 | s2t.end(); 220 | }); 221 | 222 | sst.end(); 223 | }); 224 | 225 | st.end(); 226 | }); 227 | 228 | t.test('#extend()', function (st) { 229 | st['throws'](function () { instance.extend(); }, TypeError, 'throws with no description'); 230 | st['throws'](function () { instance.extend({}); }, TypeError, 'throws with non-string description'); 231 | st['throws'](function () { instance.extend(''); }, TypeError, 'throws with empty description'); 232 | 233 | var noDescriptor = instance.extend('…'); 234 | st.notEqual(instance, noDescriptor, 'sanity check: "extend" returns a new instance with no descriptor'); 235 | 236 | var emptyDescriptor = instance.extend('…', {}); 237 | st.notEqual(instance, emptyDescriptor, 'sanity check: "extend" returns a new instance with empty descriptor'); 238 | 239 | var extraDescriptor = instance.extend('…', { foo: 'bar' }); 240 | st.notEqual(instance, extraDescriptor, 'sanity check: "extend" returns a new instance with descriptor with extra properties'); 241 | 242 | st['throws'](function () { instance.extend('…', { beforeAll: true }); }, TypeError, 'throws with method override: noncallable function'); 243 | st['throws'](function () { instance.extend('…', { beforeAll: [true] }); }, TypeError, 'throws with method override: array with noncallable function'); 244 | 245 | var validDescriptor = instance.extend('…', { beforeAll: function () {}, beforeEach: [], afterEach: [function () {}] }); 246 | st.notEqual(instance, validDescriptor, 'sanity check: "extend" returns a new instance with valid descriptor'); 247 | 248 | st.end(); 249 | }); 250 | 251 | t.end(); 252 | }); 253 | 254 | test('withGlobal', function (t) { 255 | t.test('key exceptions', function (st) { 256 | st['throws'](withGlobal, TypeError, 'requires a string or Symbol'); 257 | st['throws'](function () { withGlobal(''); }, TypeError, 'string must not be empty'); 258 | st['throws'](function () { withGlobal(); }, TypeError, 'undefined is not a string or Symbol'); 259 | st['throws'](function () { withGlobal(null); }, TypeError, 'null is not a string or Symbol'); 260 | st['throws'](function () { withGlobal(true); }, TypeError, 'true is not a string or Symbol'); 261 | st['throws'](function () { withGlobal(42); }, TypeError, '42 is not a string or Symbol'); 262 | st['throws'](function () { withGlobal(/a/g); }, TypeError, 'regex is not a string or Symbol'); 263 | st['throws'](function () { withGlobal([]); }, TypeError, 'array is not a string or Symbol'); 264 | st['throws'](function () { withGlobal({}); }, TypeError, 'object is not a string or Symbol'); 265 | st.end(); 266 | }); 267 | 268 | t.test('value exceptions', function (st) { 269 | st['throws'](function () { withGlobal('key', {}); }, TypeError, 'requires a callable valueThunk'); 270 | st.end(); 271 | }); 272 | 273 | t.end(); 274 | }); 275 | 276 | test('withOverrides', function (t) { 277 | t.test('exceptions', function (st) { 278 | st['throws'](withOverrides, TypeError, 'requires an objectThunk to override'); 279 | st['throws'](function () { withOverrides({}); }, TypeError, 'requires a callable objectThunk to override'); 280 | st['throws'](function () { withOverrides(function () { return {}; }); }, TypeError, 'requires an objectThunk to override with'); 281 | st.end(); 282 | }); 283 | 284 | t.test('works with functions', function (st) { 285 | st.doesNotThrow(function () { 286 | wrap().withOverrides( 287 | function () { return function () {}; }, 288 | function () { return {}; } 289 | ); 290 | }, 'accepts a function to override'); 291 | st.doesNotThrow(function () { 292 | wrap().withOverrides( 293 | function () { return {}; }, 294 | function () { return function () {}; } 295 | ); 296 | }, 'accepts a function of overrides'); 297 | st.end(); 298 | }); 299 | 300 | t.end(); 301 | }); 302 | 303 | test('withOverride', function (t) { 304 | t.test('exceptions', function (st) { 305 | var getObj = function () { return {}; }; 306 | st['throws'](function () { withOverride(null, 'foo'); }, TypeError, 'requires an objectThunk'); 307 | st['throws'](function () { withOverride({}, 'foo'); }, TypeError, 'requires a callable objectThunk'); 308 | st['throws'](function () { withOverride(getObj); }, TypeError, 'undefined is not a string or Symbol'); 309 | st['throws'](function () { withOverride(getObj, null); }, TypeError, 'null is not a string or Symbol'); 310 | st['throws'](function () { withOverride(getObj, true); }, TypeError, 'true is not a string or Symbol'); 311 | st['throws'](function () { withOverride(getObj, 42); }, TypeError, '42 is not a string or Symbol'); 312 | st['throws'](function () { withOverride(getObj, /a/g); }, TypeError, 'regex is not a string or Symbol'); 313 | st['throws'](function () { withOverride(getObj, []); }, TypeError, 'array is not a string or Symbol'); 314 | st['throws'](function () { withOverride(getObj, {}); }, TypeError, 'object is not a string or Symbol'); 315 | 316 | st['throws'](function () { withOverride(getObj, 'key', {}); }, TypeError, 'requires a callable valueThunk'); 317 | st.end(); 318 | }); 319 | 320 | t.end(); 321 | }); 322 | --------------------------------------------------------------------------------