├── .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 |
3 |
4 |
5 | [](https://travis-ci.org/yamadapc/jsdoctest)
6 | [](https://coveralls.io/r/yamadapc/jsdoctest)
7 | [](http://waffle.io/yamadapc/jsdoctest)
8 | [](https://david-dm.org/yamadapc/jsdoctest)
9 | [](https://david-dm.org/yamadapc/jsdoctest#info=devDependencies)
10 | [](https://www.npmjs.org/package/jsdoctest)
11 | [](https://www.npmjs.org/package/jsdoctest)
12 | [](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 | 
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 |
--------------------------------------------------------------------------------