├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── Vagrantfile ├── bin └── jsdoctest ├── examples ├── add.js ├── async-failing.js ├── async.js ├── babel-example.js ├── complex-file.js ├── complex-file2.js ├── explosive.js ├── failing.js ├── jsdoc-map.js ├── meant-to-explode.js ├── requiring-other-modules.js ├── returns-promise.js └── undefined.js ├── jsdoctest-demo.gif ├── jsdoctest.png ├── lib ├── cli │ └── index.js ├── comment-parser.js ├── get-example-code.js ├── index.js ├── mocha.js └── util.js ├── package.json ├── run-examples └── test ├── comment-parser.test.js ├── complex-file.js ├── mocha.test.js ├── test-file-captioned.js └── test-file.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lcov 3 | .vagrant 4 | .idea -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vagrant 2 | Vagrantfile 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | #- '0.11' 4 | #- '0.10' 5 | - '4.1.2' 6 | - '5.5.0' 7 | - '6' 8 | after_script: 9 | - npm i coveralls 10 | - ./node_modules/coveralls/bin/coveralls.js < lcov 11 | notifications: 12 | webhooks: 13 | urls: 14 | - https://webhooks.gitter.im/e/aac2cbae1f8d9cfa5630 15 | on_success: always 16 | on_failure: always 17 | on_start: true 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Pedro Tacla Yamada 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: FORCE 2 | npm test 3 | 4 | vagrant-test: FORCE 5 | vagrant up 6 | vagrant ssh -c 'cd /vagrant && npm test' 7 | 8 | FORCE: 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | jsdoctest 3 |

4 | 5 | [![Build Status](https://travis-ci.org/yamadapc/jsdoctest.svg)](https://travis-ci.org/yamadapc/jsdoctest) 6 | [![Coverage Status](https://coveralls.io/repos/yamadapc/jsdoctest/badge.png)](https://coveralls.io/r/yamadapc/jsdoctest) 7 | [![Stories in Ready](https://badge.waffle.io/yamadapc/jsdoctest.svg?label=ready&title=Ready)](http://waffle.io/yamadapc/jsdoctest) 8 | [![Dependency Status](https://david-dm.org/yamadapc/jsdoctest.svg)](https://david-dm.org/yamadapc/jsdoctest) 9 | [![devDependency Status](https://david-dm.org/yamadapc/jsdoctest/dev-status.svg)](https://david-dm.org/yamadapc/jsdoctest#info=devDependencies) 10 | [![npm downloads](http://img.shields.io/npm/dm/jsdoctest.svg)](https://www.npmjs.org/package/jsdoctest) 11 | [![npm version](http://img.shields.io/npm/v/jsdoctest.svg)](https://www.npmjs.org/package/jsdoctest) 12 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/yamadapc/jsdoctest?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 13 | 14 | - - - 15 | 16 | **jsdoctest** parses [`jsdoc`](http://usejsdoc.org/) `@example` tags from 17 | annotated functions and runs them as if they were doctests. 18 | 19 | Inspired by the [doctest](https://docs.python.org/2/library/doctest.html) python 20 | library, as well as its [doctestjs](http://doctestjs.org) javascript 21 | implementation. 22 | 23 | ## Demo 24 | 25 | ![demo](/jsdoctest-demo.gif) 26 | 27 | ## Set-up 28 | Here's a two line set-up you can use: 29 | ```bash 30 | $ npm i -g jsdoctest && jsdoctest --init 31 | Adding `jsdoctest` script to your package.json... 32 | Installing `mocha` and `jsdoctest` with npm: 33 | # ... npm doing some work... 34 | You can now run doctests with `npm run jsdoctest` or `npm test` 35 | ``` 36 | This will add sensible defaults to your `package.json` which you can then edit. 37 | 38 | ## Test-case Format 39 | Examples need to be valid javascript, followed by a comment with the string 40 | ` => ` prefixing the results: 41 | ```javascript 42 | /** 43 | * @example 44 | * returns10() 45 | * // => 10 46 | * returns20() 47 | * // => 20 48 | */ 49 | ``` 50 | 51 | It doesn't matter if the comment is on the same line or the next one, so the 52 | following is also valid: 53 | ```javascript 54 | /** 55 | * @example 56 | * returns10() // => 10 57 | * returns20() 58 | * // => 20 59 | */ 60 | ``` 61 | 62 | **Async test cases** are supported prefixing the expected results with the 63 | ` async => ` string and pretending to have the `cb` callback function. 64 | ```javascript 65 | /** 66 | * @example 67 | * takesCallbackAndYields10('here', cb) 68 | * // async => 10 69 | * takesCallbackAndYields20('here', cb) 70 | * // async => 30 71 | */ 72 | ``` 73 | 74 | **Promises** are also supported, just add the same `// async =>` prefix and be 75 | sure not to use a variable named `cb` on your text expression. 76 | ```javascript 77 | /** 78 | * @example 79 | * returnsPromiseThatYields10('here') 80 | * // async => 10 81 | */ 82 | ``` 83 | 84 | ## Examples 85 | The [examples](/examples) directory has a couple of examples, which may be 86 | useful. Better documentation will be added if the project raises in complexity. 87 | 88 | ## Usage 89 | The recommended way of using jsdoctest is to use 90 | [`mocha`](https://github.com/mochajs/mocha). That is made possible with: 91 | ```bash 92 | npm i mocha jsdoctest 93 | mocha --require jsdoctest 94 | ``` 95 | 96 | There's also a rudimentary command-line interface, which can be ran with: 97 | ```bash 98 | npm i jsdoctest 99 | jsdoctest 100 | ``` 101 | 102 | ## Disabling 103 | To disable running jsdoctests, while still requiring it with `mocha` (I don't 104 | know why, but you may) you can set the `JSDOCTEST_DISABLE` environment variable 105 | to anything (`JSDOCTEST_DISABLE=true mocha --require...`). 106 | 107 | ## License 108 | This code is licensed under the MIT license for Pedro Tacla Yamada. For more 109 | information, please refer to the [LICENSE](/LICENSE) file. 110 | 111 | ## Donations 112 | Would you like to buy me a beer? Send bitcoin to 3JjxJydvoJjTrhLL86LGMc8cNB16pTAF3y 113 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | Vagrant.configure(2) do |config| 2 | config.vm.box = "ubuntu/trusty64" 3 | config.vm.provision "shell", inline: <<-SHELL 4 | curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash - 5 | sudo apt-get install -y nodejs 6 | SHELL 7 | end 8 | -------------------------------------------------------------------------------- /bin/jsdoctest: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | var cli = require('../lib/cli'); 4 | cli.run(process.argv); 5 | -------------------------------------------------------------------------------- /examples/add.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @module {CommonJS} add-example 4 | */ 5 | 6 | /** 7 | * Adds two numbers 8 | * 9 | * @param {Number} x 10 | * @param {Number} y 11 | * 12 | * @example 13 | * add(10, 20) 14 | * // => 30 15 | * add(10, 50) // => 60 16 | * 10 + add(10, 50) // => 70 17 | */ 18 | 19 | function add(x, y) { 20 | return x + y; 21 | } 22 | exports.add = add; 23 | -------------------------------------------------------------------------------- /examples/async-failing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @example 3 | * takesCallback('something', cb) 4 | * // async => 'something - here' 5 | */ 6 | 7 | function takesCallback(something, cb) { 8 | cb(new Error('bad things happen')); 9 | } 10 | -------------------------------------------------------------------------------- /examples/async.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @example 3 | * takesCallback('something', cb) 4 | * // async => 'something - here' 5 | */ 6 | 7 | function takesCallback(something, cb) { 8 | cb(null, something + ' - here'); 9 | } 10 | -------------------------------------------------------------------------------- /examples/babel-example.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class HereIAm { 4 | /** 5 | * You do stuff 6 | * @example 7 | * new HereIAm().method() // => 3 8 | */ 9 | method() { 10 | return 1 + 2; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/complex-file.js: -------------------------------------------------------------------------------- 1 | if (typeof describe != undefined) var assert = require('assert'); 2 | 3 | /** 4 | * @example 5 | * createResource(); 6 | * // async => 'something' 7 | * 8 | * createResource().then(() => { 9 | * return 'something else' 10 | * }); 11 | * // async => 'something else' 12 | * 13 | * createResource() 14 | * .then(function(ret) { 15 | * assert(ret === 'something'); 16 | * return 'something else' 17 | * }); 18 | * // async => 'something else' 19 | */ 20 | 21 | function createResource() { 22 | return new Promise((resolve) => { 23 | resolve('something'); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /examples/complex-file2.js: -------------------------------------------------------------------------------- 1 | if (typeof describe != undefined) var assert = require('assert'); 2 | 3 | /** 4 | * @example 5 | * createResource(); 6 | * // async => 'something' 7 | * 8 | * createResource().then(() => { 9 | * return 'something else' 10 | * }); 11 | * // async => 'something else' 12 | * 13 | * createResource() 14 | * .then(function(ret) { 15 | * assert(ret === 'something'); 16 | * return 'something else' 17 | * }); 18 | * // async => 'something else' 19 | * 20 | * [1, 2, 3].map((x) => { 21 | * const y = x + 4 22 | * return y 23 | * }) 24 | * // => [5, 6, 7] 25 | */ 26 | 27 | function createResource() { 28 | return new Promise((resolve) => { 29 | resolve('something'); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /examples/explosive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @example 3 | * shit() 4 | * // => here 5 | */ 6 | 7 | function shit() { return 'wat'; } 8 | -------------------------------------------------------------------------------- /examples/failing.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Adds two numbers 4 | * 5 | * @param {Number} x 6 | * @param {Number} y 7 | * 8 | * @example 9 | * add(10, 20) 10 | * // => 10 11 | */ 12 | 13 | function add(x, y) { 14 | return x + y; 15 | } 16 | exports.add = add; 17 | -------------------------------------------------------------------------------- /examples/jsdoc-map.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @example 3 | * map([1, 2, 3], function(i) { 4 | * return i + 10 5 | * 20 6 | * }); 7 | * // => [11, 12, 13] 8 | */ 9 | 10 | function map(arr, fn) { 11 | var ret = []; 12 | 13 | for(var i = 0, len = arr.length; i < len; i++) { 14 | var el = fn(arr[i]); // O map de verdade seria `fn(arr[i], i, arr)` 15 | ret.push(el); 16 | } 17 | 18 | return ret; 19 | } 20 | -------------------------------------------------------------------------------- /examples/meant-to-explode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | require('./explosive'); 3 | -------------------------------------------------------------------------------- /examples/requiring-other-modules.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | /** 4 | * Runs _.map 5 | * 6 | * @example 7 | * map([1, 2, 3, 4], function(x) { return x + 10; }) //=> [11, 12, 13, 14] 8 | */ 9 | 10 | function map(arr, fn) { 11 | return _.map(arr, fn); 12 | } 13 | -------------------------------------------------------------------------------- /examples/returns-promise.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'); 2 | 3 | /** 4 | * @example 5 | * resolvesAPromise() // async => 10 6 | */ 7 | 8 | function resolvesAPromise() { 9 | return new Promise(function(resolve) { 10 | resolve(10); 11 | }); 12 | } 13 | 14 | /** 15 | * @example 16 | * rejectsAPromise() // async => 'doesn\'t matter' 17 | */ 18 | 19 | function rejectsAPromise() { 20 | return new Promise(function(resolve, reject) { 21 | reject(new Error('Blowing-up stuff')); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /examples/undefined.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Returns undefined 5 | * 6 | * @example 7 | * getUndefined() 8 | * // => undefined 9 | */ 10 | 11 | function getUndefined() { } 12 | exports.getUndefined = getUndefined; 13 | -------------------------------------------------------------------------------- /jsdoctest-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamadapc/jsdoctest/38f00cfd68074fa46122b3cd60aceb944e436786/jsdoctest-demo.gif -------------------------------------------------------------------------------- /jsdoctest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yamadapc/jsdoctest/38f00cfd68074fa46122b3cd60aceb944e436786/jsdoctest.png -------------------------------------------------------------------------------- /lib/cli/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var child = require('child_process'); 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var program = require('commander'); 6 | var beautify = require('js-beautify'); 7 | 8 | var jsdoctest = require('../..'); 9 | var mocha = require('../mocha'); 10 | var packageJson = require('../../package.json'); 11 | 12 | /** 13 | * The main command-line utility's entry point. 14 | * 15 | * @param {Array.} The `process.argv` array. 16 | */ 17 | 18 | exports.run = function(argv) { 19 | program 20 | .usage('[options] [FILES...]') 21 | .option( 22 | '-i, --init', 23 | 'Sets-up your package.json file to use `jsdoctest`' 24 | ) 25 | .option( 26 | '-s, --spec', 27 | 'Outputs generated `mocha` specs to stdout rather than running tests' 28 | ) 29 | .version(packageJson.version); 30 | 31 | program.parse(argv); 32 | 33 | if(program.init) { 34 | return exports._init(); 35 | } 36 | 37 | if(program.args.length === 0) { 38 | return program.help(); 39 | } 40 | 41 | if(program.spec) { 42 | return exports._spec(program); 43 | } 44 | 45 | // Base test running case 46 | for(var i = 0, len = program.args.length; i < len; i++) { 47 | var filename = path.resolve(process.cwd(), program.args[i]); 48 | var failed = jsdoctest.run(filename); 49 | 50 | if(failed) { 51 | exports._fail(new Error('Tests failed')); 52 | } else { 53 | console.log('Tests passed'); 54 | } 55 | } 56 | }; 57 | 58 | /** 59 | * Sets-up the project on the CWD to use `jsdoctest`. 60 | */ 61 | 62 | exports._init = function init() { 63 | var cwd = process.cwd(); 64 | var pkgJsonPth = path.join(cwd, 'package.json'); 65 | 66 | if(!fs.existsSync(pkgJsonPth)) { 67 | exports._fail('Couldn\'t find local package.json file.'); 68 | } 69 | 70 | var json = require(pkgJsonPth); 71 | if(!json.scripts) json.scripts = {}; 72 | if(json.scripts.test) json.scripts.test += ' && '; 73 | else json.scripts.test = ''; 74 | json.scripts.test += 'npm run jsdoctest'; 75 | json.scripts.jsdoctest = 76 | 'mocha --require jsdoctest `find lib src source -name "*.js"`'; 77 | 78 | console.log('Adding `jsdoctest` script to your package.json...'); 79 | fs.writeFileSync(pkgJsonPth, JSON.stringify(json, null, 2)); 80 | 81 | console.log('Installing `mocha` and `jsdoctest` with npm:'); 82 | var c = child.spawn('npm', ['install', '--save-dev', 'mocha', 'jsdoctest']); 83 | c.stdout.pipe(process.stdout); 84 | c.stderr.pipe(process.stderr); 85 | 86 | c.on('error', function(err) { 87 | exports._fail(err); 88 | }); 89 | 90 | c.on('exit', function(code) { 91 | if(code !== 0) { 92 | return exports._fail('npm exited with non-zero exit code ' + code); 93 | } 94 | 95 | console.log( 96 | 'You can now run doctests with `npm run jsdoctest` or `npm test`' 97 | ); 98 | }); 99 | }; 100 | 101 | /** 102 | * Outputs generated mocha specs for a set of files to stdout 103 | */ 104 | 105 | exports._spec = function spec(program) { 106 | for(var i = 0, len = program.args.length; i < len; i++) { 107 | var rootDir = process.cwd(); 108 | var filename = program.args[i]; 109 | var content = fs.readFileSync(filename, 'utf8'); 110 | 111 | console.log( 112 | beautify( 113 | 'require(\'should\');\n' + 114 | mocha.contentsToMochaSpec(rootDir, filename, content), 115 | { 116 | indent_size: 2, 117 | wrap_line_length: 80 118 | } 119 | ) 120 | ); 121 | } 122 | }; 123 | 124 | /** 125 | * Prints an error to stderr and exits. 126 | */ 127 | 128 | exports._fail = function fail(err) { 129 | console.error(err.message || err); 130 | process.exit(err.code || 1); 131 | }; 132 | -------------------------------------------------------------------------------- /lib/comment-parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var UglifyJS = require('uglify-js'); 3 | var util = require('./util'); 4 | 5 | /** 6 | * Parses doctest examples from a string. 7 | * 8 | * @param {String} input 9 | * @return {Array} examples 10 | */ 11 | 12 | exports.run = function commentParser$run(input) { 13 | return exports.parseExamples(exports.parseComments(input)); 14 | }; 15 | 16 | /** 17 | * Parses comments and code from a string. Heavily inspired by the code in @tj's 18 | * `dox` module 19 | * 20 | * @param {String} input 21 | * @return {Array} parsed 22 | */ 23 | 24 | exports.parseComments = function commentParser$parseComments(input) { 25 | input = input.replace(/\r\n/gm, '\n'); 26 | var nodes = []; 27 | 28 | var insideSinglelineComment = false; 29 | var insideMultilineComment = false; 30 | 31 | var currentNode = { type: 'code', string: '', }; 32 | 33 | 34 | for(var i = 0, len = input.length; i < len; i += 1) { 35 | if(insideMultilineComment) { 36 | if(input[i] === '*' && input[i + 1] === '/') { 37 | flush(); 38 | insideMultilineComment = false; 39 | i += 1; 40 | continue; 41 | } 42 | } 43 | else if(insideSinglelineComment) { 44 | if(input[i] === '\n') { 45 | flush(); 46 | insideSinglelineComment = false; 47 | continue; 48 | } 49 | } 50 | else if(input[i] === '/') { 51 | if(input[i + 1] === '*') { 52 | flush(); 53 | currentNode.type = 'comment'; 54 | insideMultilineComment = true; 55 | i += 1; 56 | continue; 57 | } 58 | else if(input[i + 1] === '/') { 59 | flush(); 60 | currentNode.type = 'comment'; 61 | insideSinglelineComment = true; 62 | i += 1; 63 | continue; 64 | } 65 | } 66 | 67 | currentNode.string += input[i]; 68 | } 69 | 70 | flush(); 71 | return nodes; 72 | 73 | function flush() { 74 | currentNode.string = util.trimString(currentNode.string); 75 | nodes.push(currentNode); 76 | currentNode = { type: 'code', string: '', }; 77 | } 78 | }; 79 | 80 | /** 81 | * Parses `jsdoc` "examples" for our doctests out of the parsed comments. 82 | * 83 | * @param {Array} parsedComments Parsed output from `parseComments` 84 | * @return {Array} parsedExamples 85 | */ 86 | 87 | exports.parseExamples = function commentParser$parseExamples(parsedComments) { 88 | var examples = []; 89 | var currentExample = { expectedResult: '', }; 90 | var caption; 91 | var currentCaption; 92 | 93 | for(var i = 0, len = parsedComments.length; i < len; i++) { 94 | if(parsedComments[i].type === 'code') { 95 | if(currentExample.testCase) flush(); 96 | 97 | currentExample.testCase = parsedComments[i].string 98 | //.replace(/\n/g, ';') 99 | .replace(/^.+<\/caption>\s*/, '') 100 | .replace(/;$/, ''); 101 | currentExample.displayTestCase = parsedComments[i].string 102 | .replace(/\n/g, ';') 103 | .replace(/^.+<\/caption>\s*/, '') 104 | .replace(/;$/, ''); 105 | 106 | currentCaption = parsedComments[i].string 107 | .match(/^(.+)<\/caption>/); 108 | if (currentCaption && currentCaption[1]) { 109 | caption = currentCaption[1]; 110 | } 111 | 112 | if (caption) { 113 | currentExample.label = 114 | currentExample.testCase + ' - ' + caption; 115 | } 116 | } else if(parsedComments[i].type === 'comment' && currentExample.testCase) { 117 | if(parsedComments[i].string.indexOf('=>') === 0) { 118 | currentExample.expectedResult += parsedComments[i].string.slice(3); 119 | } else if(parsedComments[i].string.indexOf('async =>') === 0) { 120 | currentExample.expectedResult += parsedComments[i].string.slice(9); 121 | currentExample.isAsync = true; 122 | } 123 | } 124 | } 125 | 126 | flush(); 127 | 128 | return examples; 129 | 130 | function flush() { 131 | if(currentExample.expectedResult) { 132 | examples.push(currentExample); 133 | } 134 | 135 | currentExample = { expectedResult: '', }; 136 | } 137 | }; 138 | -------------------------------------------------------------------------------- /lib/get-example-code.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = exports = getExampleCode; 4 | 5 | /** 6 | * Compiles down an `example`. Async examples call an imaginary `done` function 7 | * once they're done. 8 | * 9 | * @param {Object} comment 10 | * @param {String} comment.testCase 11 | * @param {String} comment.expectedResult 12 | * @param {Boolean} comment.isAsync 13 | * @return {String} 14 | */ 15 | 16 | function getExampleCode(comment) { 17 | var expectedResult = comment.expectedResult; 18 | var isAsync = comment.isAsync; 19 | var testCase = comment.testCase; 20 | 21 | if(isAsync) { 22 | return '\nfunction cb(err, result) {' + 23 | 'if(err) return done(err);' + 24 | (expectedResult === 'undefined' 25 | ? '(typeof result).should.eql("undefined");' 26 | : 'result.should.eql(' + expectedResult + ');') + 27 | 'done();' + 28 | '}\n' + 29 | 'var returnValue = ' + testCase + ';' + 30 | 'if(returnValue && returnValue.then && typeof returnValue.then === \'function\') {' + 31 | 'returnValue.then(cb.bind(null, null), cb);' + 32 | '}'; 33 | } else { 34 | return expectedResult === 'undefined' 35 | ? '(typeof ' + testCase + ').should.eql("undefined");' 36 | : '(' + testCase + ').should.eql(' + expectedResult + ');'; 37 | } 38 | } 39 | 40 | /** 41 | * Escapes a string for meta-quoting 42 | */ 43 | 44 | function escapeString(str) { 45 | return str.replace(/\\/g, '\\\\').replace(/\'/g, '\\\''); 46 | } 47 | 48 | exports.escapeString = escapeString; 49 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var dox = require('dox'); 5 | var _ = require('lodash'); 6 | var commentParser = require('./comment-parser'); 7 | var getExampleCode = require('./get-example-code'); 8 | var util = require('./util'); 9 | 10 | var escapeString = getExampleCode.escapeString; 11 | 12 | var JSDOCTEST_DISABLE = process.env.JSDOCTEST_DISABLE || false; 13 | 14 | /** 15 | * Runs jsdoctests in some file, and reports the results to the command-line. 16 | */ 17 | 18 | exports.run = function jsdoctest$run(filename) { 19 | require('should'); 20 | var content = fs.readFileSync(filename, 'utf8'); 21 | 22 | var jsdocTests = exports.getJsdoctests(content); 23 | content += _.map(jsdocTests, exports.toJsdocRegister).join(''); 24 | 25 | global.__registerTest = exports.registerTest; 26 | module._compile(util.stripBOM(content), filename); 27 | delete global.__registerTest; 28 | 29 | return exports.runRegistered(); 30 | }; 31 | 32 | /** 33 | * Parses "jsdoctests" out of a file's contents and returns them. These are 34 | * `dox` outputted `comment` nodes, overloaded with an `examples` field which 35 | * adds `testCase` and `expectedResult` pairs to them. 36 | */ 37 | 38 | exports.getJsdoctests = function jsdoctest$getJsdoctests(content) { 39 | var parsedContent = dox.parseComments(content); 40 | var functionComments = _.filter(parsedContent, function(c) { 41 | return c.ctx && ( 42 | c.ctx.type === 'method' 43 | || c.ctx.type === 'function' 44 | || c.ctx.type === 'class' 45 | || c.ctx.type === 'constructor' 46 | || c.ctx.type === 'property' 47 | ); 48 | }); 49 | 50 | var comments = _.map(functionComments, function(comment) { 51 | var exampleNodes = _.filter(comment.tags, { type: 'example' }); 52 | var examples = _.flatten(_.map(exampleNodes, function(exampleNode) { 53 | return commentParser.run(exampleNode.string); 54 | })); 55 | 56 | comment.examples = examples; 57 | return examples.length ? comment : undefined; 58 | }); 59 | 60 | var ret = _.compact(comments); 61 | ret.source = parsedContent; 62 | return ret; 63 | }; 64 | 65 | var tests = []; 66 | 67 | /** 68 | * Registers a test case to be run by the runner 69 | */ 70 | 71 | exports.registerTest = function jsdoctest$registerTest(id, fn) { 72 | tests.push({ 73 | id: id, 74 | fn: fn, 75 | }); 76 | }; 77 | 78 | /** 79 | * Runs test cases accumulated in the `tests` array. 80 | */ 81 | 82 | exports.runRegistered = function() { 83 | var failed = false; 84 | 85 | _.each(tests, function(test) { 86 | console.log(test.id); 87 | try { 88 | test.fn(); 89 | } catch(err) { 90 | console.error(err.toString()); 91 | failed = true; 92 | } 93 | }); 94 | 95 | return failed; 96 | }; 97 | 98 | /** 99 | * Compiles a jsdoc comment `dox` comment overloaded with the `examples` node to 100 | * the internal test suite registering code. 101 | */ 102 | 103 | exports.toJsdocRegister = function jsdoctest$toJsdocRegister(comment) { 104 | var baseId = comment.ctx.name + ' - '; 105 | var compiled = _.map(comment.examples, function(example) { 106 | var id = escapeString(baseId) + 107 | escapeString(example.testCase) + ' => ' + 108 | escapeString(example.expectedResult); 109 | var fn = 'function() {' + example.testCode + '}'; 110 | return '__registerTest(\'' + id + '\', ' + fn + ');'; 111 | }).join(''); 112 | 113 | return '\n' + compiled; 114 | }; 115 | 116 | // Mocha `--require` support: 117 | if(path.basename(process.argv[1]) === '_mocha' && !JSDOCTEST_DISABLE) { 118 | var mocha = require('./mocha'); 119 | // Avoid circular require weirdness 120 | if(module.parent.exports === mocha) { 121 | // We could just always delete the cache, but I think this is clearer and 122 | // shows explicitly what the circular problem is 123 | delete require.cache[path.join(__dirname, 'mocha.js')]; 124 | mocha = require('./mocha'); 125 | } 126 | 127 | mocha.toggleDoctestInjection(); 128 | } 129 | -------------------------------------------------------------------------------- /lib/mocha.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var _ = require('lodash'); 5 | var jsdoctest = require('./index'); 6 | var getExampleCode = require('./get-example-code'); 7 | var util = require('./util'); 8 | 9 | var escapeString = getExampleCode.escapeString; 10 | 11 | /** 12 | * Mocks `mocha`'s register environment for doctest mocha integration. This 13 | * works in the same manner `coffee-script/register` or `mocha --require 14 | * blanket` work. 15 | */ 16 | 17 | exports.loadDoctests = function loadDoctests(module, filename) { 18 | require('should'); 19 | var rootDir = process.cwd(); 20 | 21 | var content = fs.readFileSync(filename, 'utf8'); 22 | var mochaSpec = exports.contentsToMochaSpec(rootDir, filename, content); 23 | 24 | module._compile(util.stripBOM(content + mochaSpec), filename); 25 | }; 26 | 27 | /** 28 | * Compiles a string containing the contents of a JSDoc annotated file and 29 | * outputs the generated mocha spec for its JSDocTests. 30 | */ 31 | 32 | exports.contentsToMochaSpec = 33 | function contentsToMochaSpec(rootDir, filename, content) { 34 | var comments = jsdoctest.getJsdoctests(content); 35 | var moduleName = exports._getModuleName(rootDir, filename, comments.source); 36 | 37 | return '\ndescribe(\'' + escapeString(moduleName) + '\', function() {' + 38 | _.map(_.compact(comments), function(comment) { 39 | return exports.commentToMochaSpec(comment); 40 | }).join('') + 41 | '});'; 42 | }; 43 | 44 | /** 45 | * Compiles a jsdoc comment parsed by `dox` and its doctest examples into a 46 | * mocha spec. 47 | */ 48 | 49 | exports.commentToMochaSpec = function commentToMochaSpec(comment) { 50 | var ctx = comment.ctx || {}; 51 | return '\ndescribe(\'' + escapeString(ctx.string) + '\', function() {' + 52 | _.map(comment.examples, function(example) { 53 | return 'it(\'' + escapeString(example.label || example.displayTestCase) + 54 | '\', function(' + (example.isAsync ? 'done' : '') + ') {' + 55 | getExampleCode(example) + 56 | '});'; 57 | }).join('\n') + 58 | '});'; 59 | }; 60 | 61 | var originalLoad; 62 | 63 | /** 64 | * Toggles doctest injection into loaded modules. That is: doctests will be 65 | * compiled into modules as mocha specs, whenever they're declared. 66 | */ 67 | 68 | exports.toggleDoctestInjection = function toggleDoctestInjection() { 69 | if(originalLoad) { 70 | require.extensions['.js'] = originalLoad; 71 | } else { 72 | originalLoad = originalLoad || require.extensions['.js']; 73 | require.extensions['.js'] = function(module, filename) { 74 | if(filename.match(/node_modules/)) { 75 | return originalLoad(module, filename); 76 | } 77 | 78 | return exports.loadDoctests(module, filename); 79 | }; 80 | } 81 | }; 82 | 83 | /** 84 | * Resolves the expected module name for a given file, to use as the top-level 85 | * spec when generating mocha doctest `describes` 86 | * 87 | * @param {String} The root directory 88 | * @param {String} The module's filename 89 | * @return {String} moduleName 90 | */ 91 | 92 | exports._getModuleName = function getModuleName(rootDir, filename, parsedContent) { 93 | var moduleBlock = _.find(parsedContent, function(block) { 94 | return block.tags && _.find(block.tags, { type: 'module' }); 95 | }); 96 | if(moduleBlock) { 97 | var moduleTag = _.find(moduleBlock.tags, { type: 'module' }); 98 | if(moduleTag && moduleTag.string) { 99 | var smoduleTag = moduleTag.string.split(' '); 100 | if(smoduleTag[0].charAt(0) === '{' && !!smoduleTag[1]) return smoduleTag[1]; 101 | else if(!!smoduleTag[0]) return smoduleTag[0]; 102 | } 103 | } 104 | 105 | 106 | var filenamePrime = path.relative(rootDir, filename); 107 | return stripExtension(filenamePrime); 108 | 109 | function stripExtension(f) { 110 | return f.replace(/\..+$/, ''); 111 | } 112 | }; 113 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Removes trailing and leading spaces from a String. 4 | * 5 | * @param {String} str 6 | * @return {String} 7 | * 8 | * @example 9 | * trimString(' something here ') 10 | * // => 'something here' 11 | */ 12 | 13 | exports.trimString = function trimString(str) { 14 | return str.replace(/(^\s*)|(\s*$)/g, ''); 15 | }; 16 | 17 | // Copied from node.js' built-in `lib/module.js` module 18 | exports.stripBOM = function stripBOM(content) { 19 | // Remove byte order marker. This catches EF BB BF (the UTF-8 BOM) 20 | // because the buffer-to-string conversion in `fs.readFileSync()` 21 | // translates it to FEFF, the UTF-16 BOM. 22 | if (content.charCodeAt(0) === 0xFEFF) { 23 | content = content.slice(1); 24 | } 25 | return content; 26 | }; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsdoctest", 3 | "version": "1.7.1", 4 | "description": "Run jsdoc examples as doctests", 5 | "main": "lib/index.js", 6 | "bin": "bin/jsdoctest", 7 | "scripts": { 8 | "bin": "./bin/jsdoctest", 9 | "test": "JSDOCTEST_DISABLE=true mocha --require blanket -R mocha-spec-cov-alt && bash run-examples", 10 | "coverage": "JSDOCTEST_DISABLE=true mocha --require blanket -R html-cov > coverage.html" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/yamadapc/jsdoctest.git" 15 | }, 16 | "keywords": [ 17 | "test", 18 | "jsdoc", 19 | "doctests", 20 | "testing", 21 | "workflow" 22 | ], 23 | "author": "Pedro Tacla Yamada ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/yamadapc/jsdoctest/issues" 27 | }, 28 | "homepage": "https://github.com/yamadapc/jsdoctest", 29 | "dependencies": { 30 | "commander": "^2.8.1", 31 | "dox": "^0.9.0", 32 | "js-beautify": "^1.5.4", 33 | "lodash": "^4.0.1", 34 | "should": "^11.2.1", 35 | "uglify-js": "^3.0.13" 36 | }, 37 | "devDependencies": { 38 | "babel-register": "^6.9.0", 39 | "blanket": "1.1.7", 40 | "bluebird": "^3.2.1", 41 | "mocha": "^3.3.0", 42 | "mocha-spec-cov-alt": "^1.1.1" 43 | }, 44 | "config": { 45 | "blanket": { 46 | "data-cover-never": [ 47 | "node_modules" 48 | ], 49 | "pattern": [ 50 | "lib", 51 | "bin" 52 | ], 53 | "spec-cov": { 54 | "threshold": 70, 55 | "lcovOutput": "lcov" 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /run-examples: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | for i in examples/*; do 3 | printf "$i..." 4 | mocha --require . $i > /dev/null 5 | ecode=$? 6 | if [[ $i == *"failing"* ]] || [[ $i == *"explosive"* ]] || 7 | [[ $i == *"explode"* ]] || [[ $i == *"returns-promise"* ]]; then 8 | if [[ $ecode == 0 ]]; then 9 | echo "Failure: $i" 10 | echo "Expected exitcode != 0, but saw $ecode" 11 | exit 1 12 | fi 13 | else 14 | if [[ $ecode != 0 ]]; then 15 | echo "Failure: $i" 16 | echo "Expected exitcode == 0, but saw $ecode" 17 | exit 1 18 | fi 19 | fi 20 | printf " ok\n" 21 | done 22 | -------------------------------------------------------------------------------- /test/comment-parser.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; /* global describe, it */ 2 | require('should'); 3 | var commentParser = require('../lib/comment-parser'); 4 | 5 | describe('jsdoctest/comment-parser', function() { 6 | describe('.parseComments(input)', function() { 7 | it('differentiates code blocks from comments', function() { 8 | commentParser.parseComments( 9 | 'function something() {' + 10 | '/* multi-line comment here\n' + 11 | ' */\n' + 12 | '}\n' + 13 | '\n' + 14 | 'something() // single-line comment here' 15 | ).should.eql([ 16 | { type: 'code', string: 'function something() {', }, 17 | { type: 'comment', string: 'multi-line comment here', }, 18 | { type: 'code', string: '}\n\nsomething()', }, 19 | { type: 'comment', string: 'single-line comment here', }, 20 | ]); 21 | }); 22 | }); 23 | 24 | describe('.parseExamples(parsedComments)', function() { 25 | it('extracts examples out of parsed comments', function() { 26 | commentParser.parseExamples(commentParser.parseComments( 27 | 'a()\n' + 28 | '// ignored\n' + 29 | 'b()\n' + 30 | '// => 20' 31 | )).should.eql([ 32 | { displayTestCase: 'b()', testCase: 'b()', expectedResult: '20', }, 33 | ]); 34 | }); 35 | 36 | it('handles multiple line examples', function() { 37 | commentParser.parseExamples(commentParser.parseComments( 38 | 'map([1, 2, 3], function(x) {\n' + 39 | ' return x + 10\n' + 40 | '});\n' + 41 | '// => [11, 12, 13]' 42 | )).should.eql([ 43 | { 44 | displayTestCase: 'map([1, 2, 3], function(x) {; return x + 10;})', 45 | testCase: 'map([1, 2, 3], function(x) {\n return x + 10\n})', 46 | expectedResult: '[11, 12, 13]', 47 | }, 48 | ]); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/complex-file.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @example 3 | * createResource(); 4 | * // async => 'something' 5 | * 6 | * createResource().then(() => { 7 | * return 'something else' 8 | * }); 9 | * // async => 'something else' 10 | * 11 | * createResource() 12 | * .then(function(ret) { 13 | * assert(ret === 'something'); 14 | * return 'something else' 15 | * }); 16 | * // async => 'something else' 17 | */ 18 | 19 | function createResource() { 20 | return new Promise((resolve) => { 21 | resolve('something'); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /test/mocha.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; /* global describe, it */ 2 | var path = require('path'); 3 | var should = require('should'); 4 | var mocha = require('../lib/mocha'); 5 | 6 | describe('jsdoctest/mocha', function() { 7 | describe('.loadDoctests(module, filename)', function() { 8 | it('loads jsdoctests from a file and append them as mocha specs', function() { 9 | var called = false; 10 | var mockModule = { 11 | _compile: onCompile 12 | }; 13 | 14 | mocha.loadDoctests(mockModule, path.join(__dirname, 'test-file.js')); 15 | 16 | function onCompile(content, filename) { 17 | content.should.containEql( 18 | '\ndescribe(\'add()\', function() {' + 19 | 'it(\'add(1, 2)\', function() {' + 20 | '(add(1, 2)).should.eql(3);' + 21 | '});' + 22 | '});' 23 | ); 24 | called = true; 25 | filename.should.equal(path.join(__dirname, 'test-file.js')); 26 | } 27 | 28 | called.should.be.true; 29 | }); 30 | 31 | it('handles s in @example tags', function() { 32 | var called = false; 33 | var mockModule = { 34 | _compile: onCompile 35 | }; 36 | 37 | mocha.loadDoctests(mockModule, path.join(__dirname, 'test-file-captioned.js')); 38 | 39 | function onCompile(content, filename) { 40 | content.should.containEql( 41 | '\ndescribe(\'add()\', function() {' + 42 | 'it(\'add(1, 2) - Integers\', function() {' + 43 | '(add(1, 2)).should.eql(3);' + 44 | '});\n' + 45 | 'it(\'add(3, 2) - Integers\', function() {' + 46 | '(add(3, 2)).should.eql(5);' + 47 | '});\n' + 48 | 'it(\'add(1.5, 2.5) - Doubles\', function() {' + 49 | '(add(1.5, 2.5)).should.eql(4);' + 50 | '});' + 51 | '});' 52 | ); 53 | called = true; 54 | filename.should.equal(path.join(__dirname, 'test-file-captioned.js')); 55 | } 56 | 57 | called.should.be.true; 58 | }); 59 | 60 | it('handles complex examples', function() { 61 | var called = false; 62 | var mockModule = { 63 | _compile: onCompile 64 | }; 65 | 66 | mocha.loadDoctests(mockModule, path.join(__dirname, './complex-file.js')); 67 | 68 | function onCompile(content, filename) { 69 | content.should.containEql( 70 | 'var returnValue = createResource().then(() => {\n' + 71 | ' return \'something else\'\n' + 72 | ' });if(returnValue && returnValue.then && typeof returnValue.then === \'function\') {returnValue.then(cb.bind(null, null), cb);}});' 73 | ); 74 | called = true; 75 | filename.should.equal(path.join(__dirname, 'complex-file.js')); 76 | } 77 | 78 | called.should.be.true; 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/test-file-captioned.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @example Integers 3 | * add(1, 2) 4 | * // => 3 5 | * add(3, 2) 6 | * // => 5 7 | * 8 | * @example Doubles 9 | * add(1.5, 2.5) 10 | * // => 4 11 | */ 12 | 13 | function add(x, y) { 14 | return x + y; 15 | } 16 | -------------------------------------------------------------------------------- /test/test-file.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @example 3 | * add(1, 2) 4 | * // => 3 5 | */ 6 | 7 | function add(x, y) { 8 | return x + y; 9 | } 10 | --------------------------------------------------------------------------------