├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── gulpfile.js ├── lib ├── AsyncQueue.js ├── AsyncQueue.test.js ├── Call.js ├── Jar.js ├── Listener.js ├── index.js ├── matchers.js ├── probe.js ├── sensor.js └── stringify.js ├── package.json └── test ├── index.js ├── jar.js ├── mixed.js └── sensor.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015-without-regenerator"], 3 | "plugins": ["transform-async-to-generator", "transform-export-extensions"] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/stringify.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "node": true, 5 | "mocha": true, 6 | "es6": true 7 | }, 8 | "parser": "babel-eslint", 9 | "rules": { 10 | "array-bracket-spacing": [ 11 | 2, 12 | "never" 13 | ], 14 | "brace-style": [ 15 | 2, 16 | "1tbs" 17 | ], 18 | "consistent-return": 0, 19 | "indent": [ 20 | 2, 21 | 2 22 | ], 23 | "no-multiple-empty-lines": [ 24 | 2, 25 | { 26 | "max": 2 27 | } 28 | ], 29 | "no-use-before-define": [ 30 | 2, 31 | "nofunc" 32 | ], 33 | "one-var": [ 34 | 2, 35 | "never" 36 | ], 37 | "quote-props": [ 38 | 2, 39 | "as-needed" 40 | ], 41 | "quotes": [ 42 | 2, 43 | "single" 44 | ], 45 | "space-after-keywords": [ 46 | 2, 47 | "always" 48 | ], 49 | "space-before-function-paren": [ 50 | 2, 51 | { 52 | "anonymous": "always", 53 | "named": "never" 54 | } 55 | ], 56 | "space-in-parens": [ 57 | 2, 58 | "never" 59 | ], 60 | "strict": [ 61 | 2, 62 | "global" 63 | ], 64 | "curly": [ 65 | 2, 66 | "all" 67 | ], 68 | "eol-last": 2, 69 | "key-spacing": [ 70 | 2, 71 | { 72 | "beforeColon": false, 73 | "afterColon": true 74 | } 75 | ], 76 | "no-eval": 2, 77 | "no-with": 2, 78 | "space-infix-ops": 2, 79 | "dot-notation": [ 80 | 2, 81 | { 82 | "allowKeywords": true 83 | } 84 | ], 85 | "eqeqeq": 2, 86 | "no-alert": 2, 87 | "no-caller": 2, 88 | "no-empty-label": 2, 89 | "no-extend-native": 2, 90 | "no-extra-bind": 2, 91 | "no-implied-eval": 2, 92 | "no-iterator": 2, 93 | "no-label-var": 2, 94 | "no-labels": 2, 95 | "no-lone-blocks": 2, 96 | "no-loop-func": 2, 97 | "no-multi-spaces": 2, 98 | "no-multi-str": 2, 99 | "no-native-reassign": 2, 100 | "no-new": 2, 101 | "no-new-func": 2, 102 | "no-new-wrappers": 2, 103 | "no-octal-escape": 2, 104 | "no-proto": 2, 105 | "no-return-assign": 2, 106 | "no-script-url": 2, 107 | "no-sequences": 2, 108 | "no-unused-expressions": 2, 109 | "yoda": 2, 110 | "no-shadow": 2, 111 | "no-shadow-restricted-names": 2, 112 | "no-undef-init": 2, 113 | "camelcase": 2, 114 | "comma-spacing": 2, 115 | "new-cap": 2, 116 | "new-parens": 2, 117 | "no-array-constructor": 2, 118 | "no-extra-parens": 2, 119 | "no-new-object": 2, 120 | "no-spaced-func": 2, 121 | "no-trailing-spaces": 2, 122 | "no-underscore-dangle": 0, 123 | "semi": 2, 124 | "semi-spacing": [ 125 | 2, 126 | { 127 | "before": false, 128 | "after": true 129 | } 130 | ], 131 | "space-return-throw-case": 2 132 | }, 133 | "ecmaFeatures": { 134 | "modules": true 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4.2' -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Marius Gundersen (https://mariusgundersen.net) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # descartes [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][daviddm-image]][daviddm-url] [![Coverage percentage][coveralls-image]][coveralls-url] 2 | > Mock async JavaScript libraries 3 | 4 | 5 | ## Install 6 | 7 | ```sh 8 | $ npm install --save descartes 9 | ``` 10 | 11 | 12 | ## Usage 13 | 14 | ```js 15 | import {Jar, withArgs, withExactArgs, onThis} from 'descartes'; 16 | 17 | it("should behave like this", async function(){ 18 | const jar = new Jar(); 19 | const stub = jar.probe('stub'); 20 | const spy = jar.sensor('spy'); 21 | 22 | //start async method 23 | const result = myAsyncMethod(spy, stub); 24 | 25 | await spy.called(); 26 | await spy.called(withArgs('something')); 27 | await spy.called(withExactArgs('something', 'else')); 28 | 29 | stub.resolvesTo('something'); 30 | await stub.called(); 31 | 32 | stub.rejects(new Error('it should handle this')); 33 | await stub.called( 34 | withArgs(13), 35 | onThis(window)); 36 | 37 | const call = await spy.called(); 38 | call.args[0].should.equal('something'); 39 | call.args[1].should.be.a('Function'); 40 | call.args[1](null, 'result'); 41 | 42 | (await result).should.equal('expected value'); 43 | }); 44 | 45 | ``` 46 | 47 | ## License 48 | 49 | MIT © [Marius Gundersen](https://mariusgundersen.net) 50 | 51 | 52 | [npm-image]: https://badge.fury.io/js/descartes.svg 53 | [npm-url]: https://npmjs.org/package/descartes 54 | [travis-image]: https://travis-ci.org/mariusGundersen/descartes.svg?branch=master 55 | [travis-url]: https://travis-ci.org/mariusGundersen/descartes 56 | [daviddm-image]: https://david-dm.org/mariusGundersen/descartes.svg?theme=shields.io 57 | [daviddm-url]: https://david-dm.org/mariusGundersen/descartes 58 | [coveralls-image]: https://coveralls.io/repos/mariusGundersen/descartes/badge.svg 59 | [coveralls-url]: https://coveralls.io/r/mariusGundersen/descartes 60 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var eslint = require('gulp-eslint'); 3 | var excludeGitignore = require('gulp-exclude-gitignore'); 4 | var mocha = require('gulp-mocha'); 5 | var nsp = require('gulp-nsp'); 6 | var plumber = require('gulp-plumber'); 7 | var babel = require('gulp-babel'); 8 | var del = require('del'); 9 | 10 | // Initialize the babel transpiler so ES2015 files gets compiled 11 | // when they're loaded 12 | require('babel-core/register'); 13 | 14 | gulp.task('static', function () { 15 | return gulp.src('**/*.js') 16 | .pipe(excludeGitignore()) 17 | .pipe(eslint()) 18 | .pipe(eslint.format()) 19 | .pipe(eslint.failAfterError()); 20 | }); 21 | 22 | gulp.task('nsp', function (cb) { 23 | nsp({package: __dirname + '/package.json'}, cb); 24 | }); 25 | 26 | gulp.task('test', function (cb) { 27 | var mochaErr; 28 | 29 | gulp.src(['test/**/*.js', 'lib/**/*.test.js']) 30 | .pipe(plumber()) 31 | .pipe(mocha({reporter: 'spec'})) 32 | .on('error', function (err) { 33 | mochaErr = err; 34 | }) 35 | .on('end', function () { 36 | cb(mochaErr); 37 | }); 38 | }); 39 | 40 | gulp.task('onlyTest', function () { 41 | return gulp.src(['test/**/*.js', 'lib/**/*.test.js']) 42 | .pipe(plumber()) 43 | .pipe(mocha({reporter: 'spec'})); 44 | }); 45 | 46 | gulp.task('watch', ['onlyTest'], function () { 47 | return gulp.watch(['lib/**/*.js', 'test/**/*.js'], ['onlyTest']); 48 | }); 49 | 50 | gulp.task('babel', ['clean'], function () { 51 | return gulp.src('lib/**/*.js') 52 | .pipe(babel()) 53 | .pipe(gulp.dest('dist')); 54 | }); 55 | 56 | gulp.task('clean', function () { 57 | return del('dist'); 58 | }); 59 | 60 | gulp.task('prepublish', ['nsp', 'babel']); 61 | gulp.task('default', ['static', 'test']); 62 | -------------------------------------------------------------------------------- /lib/AsyncQueue.js: -------------------------------------------------------------------------------- 1 | export default class AsyncQueue { 2 | constructor() { 3 | this._items = []; 4 | this._receivers = []; 5 | } 6 | 7 | push(value) { 8 | if (this._receivers.length === 0) { 9 | this._items.push(value); 10 | } else { 11 | this._receivers.shift()(value); 12 | } 13 | } 14 | 15 | pop() { 16 | return new Promise(resolve => 17 | this._items.length === 0 18 | ? this._receivers.push(resolve) 19 | : resolve(this._items.shift())); 20 | } 21 | 22 | get isEmpty() { 23 | return this._items.length === 0 24 | && this._receivers.length === 0; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/AsyncQueue.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import AsyncQueue from './AsyncQueue'; 3 | 4 | describe('Async Queue', function (){ 5 | it('should create an instance', function (){ 6 | assert.ok(new AsyncQueue()); 7 | }); 8 | 9 | it('should start empty', function (){ 10 | assert.ok(new AsyncQueue().isEmpty); 11 | }); 12 | 13 | describe('push before pop', function (){ 14 | before(function (){ 15 | this.queue = new AsyncQueue(); 16 | this.queue.push('test'); 17 | }); 18 | 19 | it('should not be empty', function (){ 20 | assert.equal(this.queue.isEmpty, false); 21 | }); 22 | 23 | it('should pop the right value', async function (){ 24 | const value = await this.queue.pop(); 25 | assert.equal(value, 'test'); 26 | }); 27 | 28 | it('should be empty again', function (){ 29 | assert.ok(this.queue.isEmpty); 30 | }); 31 | }); 32 | 33 | describe('push after pop', function (){ 34 | before(function (){ 35 | this.queue = new AsyncQueue(); 36 | this.value = this.queue.pop(); 37 | }); 38 | 39 | it('should not be empty', function (){ 40 | assert.equal(this.queue.isEmpty, false); 41 | }); 42 | 43 | it('should pop the right value', async function (){ 44 | this.queue.push('test'); 45 | assert.equal(await this.value, 'test'); 46 | }); 47 | 48 | it('should be empty again', function (){ 49 | assert.ok(this.queue.isEmpty); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /lib/Call.js: -------------------------------------------------------------------------------- 1 | export default class Call{ 2 | constructor(target, self, args){ 3 | this.target = target; 4 | this.self = self; 5 | this.args = args; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/Jar.js: -------------------------------------------------------------------------------- 1 | import probe from './probe'; 2 | import sensor from './sensor'; 3 | import AsyncQueue from './AsyncQueue'; 4 | 5 | export default class Jar { 6 | constructor(config = {timelimit: 1000, queue: new AsyncQueue()}) { 7 | this._queue = config.queue; 8 | this._timelimit = config.timelimit; 9 | } 10 | 11 | probe(displayName) { 12 | return probe(displayName, {queue: this._queue, timelimit: this._timelimit}); 13 | } 14 | 15 | sensor(displayName) { 16 | return sensor(displayName, {queue: this._queue, timelimit: this._timelimit}); 17 | } 18 | 19 | done() { 20 | if (this._queue.isEmpty === false){ 21 | throw new Error('Jar is not empty'); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/Listener.js: -------------------------------------------------------------------------------- 1 | import stringify from './stringify'; 2 | import AsyncQueue from './AsyncQueue'; 3 | 4 | export default class Listener{ 5 | constructor(callQueue = new AsyncQueue()){ 6 | this._callQueue = callQueue; 7 | } 8 | 9 | async awaitCall(target, tests, timelimit){ 10 | const latest = await timeout(this._callQueue.pop(), () => createTimeoutError(target, tests, timelimit), timelimit); 11 | 12 | latest.resolve(); 13 | const firstFailing = getAllFailing(tests, latest.call)[0]; 14 | if (firstFailing === undefined){ 15 | return latest.call; 16 | } else { 17 | throw new Error(`Expected ${target.displayName} ${firstFailing.expectedIt(stringify)}, actually ${firstFailing.actually(latest.call, stringify)}`); 18 | } 19 | } 20 | 21 | called(call){ 22 | return new Promise(resolve => this._callQueue.push({call: call, resolve: resolve})); 23 | } 24 | } 25 | 26 | function getAllFailing(tests, call){ 27 | return tests.filter(t => !t.test(call)); 28 | } 29 | 30 | function timeout(promise, error, timelimit){ 31 | return new Promise((resolve, reject) => { 32 | promise.then(resolve); 33 | setTimeout(() => reject(error()), timelimit); 34 | }); 35 | } 36 | 37 | function createTimeoutError(target, tests, timelimit){ 38 | const firstExpected = tests.length === 0 39 | ? 'to have been called' 40 | : tests[0].expectedIt(stringify); 41 | return new Error(`Expected ${target.displayName} ${firstExpected}, actually not called within ${timelimit}ms`); 42 | } 43 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | export probe from './probe'; 2 | 3 | export sensor from './sensor'; 4 | 5 | export Jar from './Jar'; 6 | 7 | export {withArgs, withExactArgs, onThis} from './matchers'; 8 | -------------------------------------------------------------------------------- /lib/matchers.js: -------------------------------------------------------------------------------- 1 | export function withArgs(...args){ 2 | return { 3 | test(call){ 4 | return arrayMatch(args, call.args); 5 | }, 6 | expectedIt: stringify => `to be called with at least (${args.map(stringify).join(', ')})`, 7 | actually: (call, stringify) => `called with (${call.args.map(stringify).join(', ')})` 8 | }; 9 | } 10 | 11 | export function withExactArgs(...args){ 12 | return { 13 | test(call){ 14 | return arrayMatch(args, call.args) && arrayMatch(call.args, args); 15 | }, 16 | expectedIt: stringify => `to be called with exactly (${args.map(stringify).join(', ')})`, 17 | actually: (call, stringify) => `called with (${call.args.map(stringify).join(', ')})` 18 | }; 19 | } 20 | 21 | export function onThis(that){ 22 | return { 23 | test: call => call.self === that, 24 | expectedIt: stringify => `to be called with ${stringify(that)} as this`, 25 | actually: (call, stringify) => `called with ${stringify(call.self)} as this` 26 | }; 27 | } 28 | 29 | export function onTarget(target){ 30 | return { 31 | test: call => call.target === target, 32 | expectedIt: () => 'to be called', 33 | actually: call => `called ${call.target.displayName}` 34 | }; 35 | } 36 | 37 | function arrayMatch(a, b){ 38 | return a.every((x, i) => x === b[i]); 39 | } 40 | -------------------------------------------------------------------------------- /lib/probe.js: -------------------------------------------------------------------------------- 1 | import Call from './Call'; 2 | import Listener from './Listener'; 3 | import {onTarget} from './matchers'; 4 | 5 | export default function probe(displayName = 'probe', config = {timelimit: 1000, queue: undefined}){ 6 | const listener = new Listener(config.queue); 7 | let result = null; 8 | const theProbe = function (...args){ 9 | return listener.called(new Call(theProbe, this, args)).then(() => result); 10 | }; 11 | 12 | theProbe.called = function (...tests){ 13 | return listener.awaitCall(theProbe, [onTarget(theProbe), ...tests], config.timelimit); 14 | }; 15 | 16 | theProbe.resolves = function (value){ 17 | result = Promise.resolve(value); 18 | }; 19 | 20 | theProbe.rejects = function (value){ 21 | result = Promise.reject(value); 22 | }; 23 | 24 | theProbe.displayName = displayName; 25 | 26 | return theProbe; 27 | } 28 | -------------------------------------------------------------------------------- /lib/sensor.js: -------------------------------------------------------------------------------- 1 | import Call from './Call'; 2 | import Listener from './Listener'; 3 | import {onTarget} from './matchers'; 4 | 5 | export default function sensor(displayName = 'sensor', config = {timelimit: 1000, queue: undefined}){ 6 | const listener = new Listener(config.queue); 7 | const theSensor = function (...args){ 8 | listener.called(new Call(theSensor, this, args)); 9 | }; 10 | 11 | theSensor.called = function (...tests){ 12 | return listener.awaitCall(theSensor, [onTarget(theSensor), ...tests], config.timelimit); 13 | }; 14 | 15 | theSensor.displayName = displayName; 16 | 17 | return theSensor; 18 | } 19 | -------------------------------------------------------------------------------- /lib/stringify.js: -------------------------------------------------------------------------------- 1 | // These helpers are based on Mocha's utils: 2 | // https://github.com/mochajs/mocha/blob/master/lib/utils.js 3 | // Copyright (c) 2011-2015 TJ Holowaychuk 4 | // MIT License: https://github.com/mochajs/mocha/blob/master/LICENSE 5 | 6 | /** 7 | * Stringify `value`. Different behavior depending on type of value: 8 | * 9 | * - If `value` is undefined or null, return `'[undefined]'` or `'[null]'`, respectively. 10 | * - If `value` is not an object, function or array, return result of `value.toString()` wrapped in double-quotes. 11 | * - If `value` is an *empty* object, function, or array, return result of function 12 | * {@link emptyRepresentation}. 13 | * - If `value` has properties, call {@link canonicalize} on it, then return result of JSON.stringify(). 14 | */ 15 | module.exports = function stringify(value) { 16 | var type = getType(value); 17 | 18 | if (!~['object', 'array', 'function'].indexOf(type)) { 19 | if (type !== 'buffer') { 20 | return jsonStringify(value); 21 | } 22 | var json = value.toJSON(); 23 | // Based on the toJSON result 24 | return jsonStringify(json.data && json.type ? json.data : json, 2) 25 | .replace(/,(\n|$)/g, '$1'); 26 | } 27 | 28 | for (var prop in value) { 29 | if (Object.prototype.hasOwnProperty.call(value, prop)) { 30 | return jsonStringify(canonicalize(value), 2).replace(/,(\n|$)/g, '$1'); 31 | } 32 | } 33 | 34 | return emptyRepresentation(value, type); 35 | }; 36 | 37 | 38 | /** 39 | * Takes some variable and asks `Object.prototype.toString()` what it thinks it is. 40 | * 41 | * type({}) // 'object' 42 | * type([]) // 'array' 43 | * type(1) // 'number' 44 | * type(false) // 'boolean' 45 | * type(Infinity) // 'number' 46 | * type(null) // 'null' 47 | * type(new Date()) // 'date' 48 | * type(/foo/) // 'regexp' 49 | * type('type') // 'string' 50 | * type(global) // 'global' 51 | */ 52 | function getType(value) { 53 | if (value === undefined) { 54 | return 'undefined'; 55 | } else if (value === null) { 56 | return 'null'; 57 | } else if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) { 58 | return 'buffer'; 59 | } 60 | return Object.prototype.toString.call(value) 61 | .replace(/^\[.+\s(.+?)\]$/, '$1') 62 | .toLowerCase(); 63 | } 64 | 65 | 66 | /** 67 | * like JSON.stringify but more sense. 68 | */ 69 | function jsonStringify(object, spaces, depth) { 70 | if (typeof spaces === 'undefined') { 71 | // primitive types 72 | return _stringify(object); 73 | } 74 | 75 | depth = depth || 1; 76 | var space = spaces * depth; 77 | var str = Array.isArray(object) ? '[' : '{'; 78 | var end = Array.isArray(object) ? ']' : '}'; 79 | var length = object.length || Object.keys(object).length; 80 | // `.repeat()` polyfill 81 | function repeat(s, n) { 82 | return new Array(n).join(s); 83 | } 84 | 85 | function _stringify(val) { 86 | switch (getType(val)) { 87 | case 'null': 88 | case 'undefined': 89 | val = '[' + val + ']'; 90 | break; 91 | case 'array': 92 | case 'object': 93 | val = jsonStringify(val, spaces, depth + 1); 94 | break; 95 | case 'boolean': 96 | case 'regexp': 97 | case 'number': 98 | val = val === 0 && 1 / val === -Infinity // `-0` 99 | ? '-0' 100 | : val.toString(); 101 | break; 102 | case 'date': 103 | var sDate = isNaN(val.getTime()) // Invalid date 104 | ? val.toString() 105 | : val.toISOString(); 106 | val = '[Date: ' + sDate + ']'; 107 | break; 108 | case 'buffer': 109 | var json = val.toJSON(); 110 | // Based on the toJSON result 111 | json = json.data && json.type ? json.data : json; 112 | val = '[Buffer: ' + jsonStringify(json, 2, depth + 1) + ']'; 113 | break; 114 | default: 115 | val = (val === '[Function]' || val === '[Circular]') 116 | ? val 117 | : JSON.stringify(val); // string 118 | } 119 | return val; 120 | } 121 | 122 | for (var i in object) { 123 | if (!object.hasOwnProperty(i)) { 124 | continue; // not my business 125 | } 126 | --length; 127 | str += '\n ' + repeat(' ', space) 128 | + (Array.isArray(object) ? '' : '"' + i + '": ') // key 129 | + _stringify(object[i]) // value 130 | + (length ? ',' : ''); // comma 131 | } 132 | 133 | return str 134 | // [], {} 135 | + (str.length !== 1 ? '\n' + repeat(' ', --space) + end : end); 136 | } 137 | 138 | /** 139 | * Return a new Thing that has the keys in sorted order. Recursive. 140 | * 141 | * If the Thing... 142 | * - has already been seen, return string `'[Circular]'` 143 | * - is `undefined`, return string `'[undefined]'` 144 | * - is `null`, return value `null` 145 | * - is some other primitive, return the value 146 | * - is not a primitive or an `Array`, `Object`, or `Function`, return the value of the Thing's `toString()` method 147 | * - is a non-empty `Array`, `Object`, or `Function`, return the result of calling this function again. 148 | * - is an empty `Array`, `Object`, or `Function`, return the result of calling `emptyRepresentation()` 149 | */ 150 | function canonicalize(value, stack) { 151 | var canonicalizedObj; 152 | /* eslint-disable no-unused-vars */ 153 | var prop; 154 | /* eslint-enable no-unused-vars */ 155 | var type = getType(value); 156 | function withStack(value, fn) { 157 | stack.push(value); 158 | fn(); 159 | stack.pop(); 160 | } 161 | 162 | stack = stack || []; 163 | 164 | if (stack.indexOf(value) !== -1) { 165 | return '[Circular]'; 166 | } 167 | 168 | switch (type) { 169 | case 'undefined': 170 | case 'buffer': 171 | case 'null': 172 | canonicalizedObj = value; 173 | break; 174 | case 'array': 175 | withStack(value, function() { 176 | canonicalizedObj = value.map(function(item) { 177 | return canonicalize(item, stack); 178 | }); 179 | }); 180 | break; 181 | case 'function': 182 | /* eslint-disable guard-for-in */ 183 | for (prop in value) { 184 | canonicalizedObj = {}; 185 | break; 186 | } 187 | /* eslint-enable guard-for-in */ 188 | if (!canonicalizedObj) { 189 | canonicalizedObj = emptyRepresentation(value, type); 190 | break; 191 | } 192 | /* falls through */ 193 | case 'object': 194 | canonicalizedObj = canonicalizedObj || {}; 195 | withStack(value, function() { 196 | Object.keys(value).sort().forEach(function(key) { 197 | canonicalizedObj[key] = canonicalize(value[key], stack); 198 | }); 199 | }); 200 | break; 201 | case 'date': 202 | case 'number': 203 | case 'regexp': 204 | case 'boolean': 205 | canonicalizedObj = value; 206 | break; 207 | default: 208 | canonicalizedObj = value.toString(); 209 | } 210 | 211 | return canonicalizedObj; 212 | }; 213 | 214 | /** 215 | * If a value could have properties, and has none, this function is called, 216 | * which returns a string representation of the empty value. 217 | * 218 | * Functions w/ no properties return `'[Function]'` 219 | * Arrays w/ length === 0 return `'[]'` 220 | * Objects w/ no properties return `'{}'` 221 | * All else: return result of `value.toString()` 222 | */ 223 | function emptyRepresentation(value, type) { 224 | type = type || getType(value); 225 | 226 | switch (type) { 227 | case 'function': 228 | return '[Function]'; 229 | case 'object': 230 | return '{}'; 231 | case 'array': 232 | return '[]'; 233 | default: 234 | return value.toString(); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "descartes", 3 | "version": "0.0.9", 4 | "description": "Mock async JavaScript libraries", 5 | "homepage": "https://github.com/mariusGundersen/descartes", 6 | "author": { 7 | "name": "Marius Gundersen", 8 | "email": "npm@mariusgundersen.net", 9 | "url": "https://mariusgundersen.net" 10 | }, 11 | "files": [ 12 | "dist" 13 | ], 14 | "main": "dist/index.js", 15 | "keywords": [ 16 | "async", 17 | "mock", 18 | "test" 19 | ], 20 | "repository": "mariusGundersen/descartes", 21 | "devDependencies": { 22 | "babel-core": "^6.2.1", 23 | "babel-eslint": "^4.1.6", 24 | "babel-plugin-transform-async-to-generator": "^6.1.18", 25 | "babel-plugin-transform-export-extensions": "^6.1.18", 26 | "babel-preset-es2015-without-regenerator": "^1.0.1", 27 | "del": "^2.0.2", 28 | "gulp": "^3.6.0", 29 | "gulp-babel": "^6.1.0", 30 | "gulp-eslint": "^1.0.0", 31 | "gulp-exclude-gitignore": "^1.0.0", 32 | "gulp-mocha": "^2.0.0", 33 | "gulp-nsp": "^2.1.0", 34 | "gulp-plumber": "^1.0.0" 35 | }, 36 | "scripts": { 37 | "prepublish": "gulp prepublish", 38 | "test": "gulp" 39 | }, 40 | "engines": { 41 | "node": ">=4.0.0" 42 | }, 43 | "license": "MIT", 44 | "dependencies": {} 45 | } 46 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import {probe, withArgs, withExactArgs, onThis} from '../lib/index'; 3 | 4 | describe('called', function () { 5 | 6 | beforeEach(function (){ 7 | this.log = probe(); 8 | }); 9 | 10 | it('should wait until the log method is called', async function () { 11 | const result = doSomethingAsync(this.log); 12 | 13 | this.log.resolves(5); 14 | await this.log.called(); 15 | 16 | assert.equal(await result, 5); 17 | }); 18 | }); 19 | 20 | describe('calledWith', function () { 21 | 22 | beforeEach(function (){ 23 | this.log = probe(); 24 | }); 25 | 26 | it('should wait until the log method is called', async function () { 27 | const result = doSomethingAsync(this.log); 28 | 29 | this.log.resolves(5); 30 | await this.log.called(withArgs('resolved')); 31 | 32 | assert.equal(await result, 5); 33 | }); 34 | }); 35 | 36 | describe('calledWithExactly', function () { 37 | 38 | beforeEach(function (){ 39 | this.log = probe(); 40 | }); 41 | 42 | it('should wait until the log method is called', async function () { 43 | const result = doSomethingAsync(this.log); 44 | 45 | await this.log.called(withExactArgs('resolved', 'with', 'args')); 46 | 47 | return await result; 48 | }); 49 | }); 50 | 51 | describe('calledOnThis', function () { 52 | 53 | beforeEach(function (){ 54 | this.log = probe(); 55 | }); 56 | 57 | it('should wait until the log method is called', async function () { 58 | const result = doSomethingAsync(this.log); 59 | 60 | await this.log.called(onThis(undefined)); 61 | 62 | return await result; 63 | }); 64 | }); 65 | 66 | describe('sync start', function (){ 67 | it('should let you set up the first await after starting the async function', async function(){ 68 | const log = probe(); 69 | const result = doSomethingMoreComplexAsync(log); 70 | 71 | await log.called(withArgs('hello')); 72 | 73 | await log.called(withArgs('bye')); 74 | 75 | return await result; 76 | }); 77 | }); 78 | 79 | async function doSomethingAsync(log){ 80 | await Promise.resolve(); 81 | return log('resolved', 'with', 'args'); 82 | } 83 | 84 | async function doSomethingMoreComplexAsync(log){ 85 | log('hello'); 86 | await Promise.resolve(); 87 | log('bye', 'bye'); 88 | return 50; 89 | } 90 | -------------------------------------------------------------------------------- /test/jar.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import Jar from '../lib/Jar'; 3 | import {withArgs, withExactArgs} from '../lib/matchers'; 4 | 5 | describe('jar', function () { 6 | 7 | beforeEach(function (){ 8 | this.jar = new Jar(); 9 | this.log = this.jar.sensor('log'); 10 | this.a = this.jar.probe('a'); 11 | this.b = this.jar.probe('b'); 12 | }); 13 | 14 | it('should check the global order of probes and sensors', async function () { 15 | const result = doSomethingAsync(this.log, this.a, this.b); 16 | 17 | await this.log.called(withArgs('calling a')); 18 | 19 | this.a.resolves(2); 20 | await this.a.called(); 21 | 22 | await this.log.called(withExactArgs('called a')); 23 | 24 | await this.log.called(withExactArgs('calling b')); 25 | 26 | this.b.resolves(3); 27 | await this.b.called(); 28 | 29 | await this.log.called(withExactArgs('called b')); 30 | 31 | this.jar.done(); 32 | assert.equal(await result, 5); 33 | }); 34 | }); 35 | 36 | async function doSomethingAsync(log, a, b){ 37 | log('calling a'); 38 | const first = await a(); 39 | log('called a'); 40 | log('calling b'); 41 | const second = await b(); 42 | log('called b'); 43 | return first + second; 44 | } 45 | -------------------------------------------------------------------------------- /test/mixed.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import probe from '../lib/probe'; 3 | import sensor from '../lib/sensor'; 4 | import {withArgs} from '../lib/matchers'; 5 | 6 | describe('mixed', function () { 7 | 8 | beforeEach(function (){ 9 | this.log = sensor(); 10 | this.a = probe(); 11 | this.b = probe(); 12 | }); 13 | 14 | it('should handle sync methods too', async function () { 15 | const result = doSomethingAsync(this.log, this.a, this.b); 16 | 17 | await this.log.called(withArgs('calling a')); 18 | 19 | this.a.resolves(2); 20 | await this.a.called(); 21 | 22 | await this.log.called(withArgs('called a')); 23 | 24 | await this.log.called(withArgs('calling b')); 25 | 26 | this.b.resolves(3); 27 | await this.b.called(); 28 | 29 | await this.log.called(withArgs('called b')); 30 | 31 | assert.equal(await result, 5); 32 | }); 33 | }); 34 | 35 | async function doSomethingAsync(log, a, b){ 36 | log('calling a'); 37 | const first = await a(); 38 | log('called a'); 39 | log('calling b'); 40 | const second = await b(); 41 | log('called b'); 42 | return first + second; 43 | } 44 | -------------------------------------------------------------------------------- /test/sensor.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import sensor from '../lib/sensor'; 3 | 4 | describe('sensor', function () { 5 | 6 | beforeEach(function (){ 7 | this.a = sensor(); 8 | this.b = sensor(); 9 | this.c = sensor(); 10 | }); 11 | 12 | it('should work with synchronous methods', async function () { 13 | const result = doSomething(this.a, this.b, this.c); 14 | 15 | await this.a.called(); 16 | 17 | await this.b.called(); 18 | 19 | await this.c.called(); 20 | 21 | assert.ok(result); 22 | }); 23 | 24 | it('should work with asynchronous methods', async function () { 25 | const result = doSomethingAsync(this.a, this.b, this.c); 26 | 27 | await this.a.called(); 28 | 29 | await this.b.called(); 30 | 31 | await this.c.called(); 32 | 33 | assert.ok(result); 34 | }); 35 | 36 | it('should handle multiple calls to the same sensor', async function () { 37 | const result = doSomething(this.a, this.a, this.a); 38 | 39 | await this.a.called(); 40 | 41 | await this.a.called(); 42 | 43 | await this.a.called(); 44 | 45 | assert.ok(result); 46 | }); 47 | }); 48 | 49 | function doSomething(a, b, c){ 50 | a(); 51 | b(); 52 | c(); 53 | 54 | return true; 55 | } 56 | 57 | async function doSomethingAsync(a, b, c){ 58 | await Promise.resolve(); 59 | a(); 60 | await Promise.resolve(); 61 | b(); 62 | await Promise.resolve(); 63 | c(); 64 | await Promise.resolve(); 65 | 66 | return true; 67 | } 68 | --------------------------------------------------------------------------------