├── .eslintignore
├── .eslintrc.json
├── .github
└── workflows
│ └── CI.yaml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── SECURITY.md
├── bin
└── cli.js
├── index.js
├── lib
├── child.js
├── coverage.js
├── generators.js
├── log.js
└── testrunner.js
├── package.json
├── readme.md
├── support
└── json
│ └── cycle.js
└── test
├── fixtures
├── async-code.js
├── async-test.js
├── child-code-global.js
├── child-code-namespace.js
├── child-tests-global.js
├── child-tests-namespace.js
├── coverage-code.js
├── coverage-multiple-code.js
├── coverage-test.js
├── generators-code.js
├── generators-test.js
├── infinite-loop-code.js
├── infinite-loop-test.js
├── testrunner-code.js
├── testrunner-tests.js
├── uncaught-exception-code.js
└── uncaught-exception-test.js
└── testrunner.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | /coverage
2 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint:recommended",
3 | "env": {
4 | "node": true,
5 | "qunit": true
6 | },
7 | "rules": {
8 | "no-control-regex": 0,
9 | "no-console": 0
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.github/workflows/CI.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | - push
4 | - pull_request
5 |
6 | jobs:
7 |
8 | test:
9 | strategy:
10 | matrix:
11 | include:
12 | - node: 10.x
13 | - node: 12.x
14 | - node: 14.x
15 | - node: 16.x
16 | - node: 18.x
17 | - node: 20.x
18 |
19 | name: Node ${{ matrix.node }}
20 | runs-on: ubuntu-latest
21 | steps:
22 | - uses: actions/checkout@v4
23 |
24 | - name: Install Node.js
25 | uses: actions/setup-node@v4
26 | with:
27 | node-version: ${{ matrix.node }}
28 |
29 | - run: npm install
30 |
31 | - run: npm test
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.DS_Store
2 | /node_modules
3 | /coverage
4 | /package-lock.json
5 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## v2.0.1
4 |
5 | Fixed:
6 |
7 | * Switch from [`QUnit.load()`](https://qunitjs.com/api/QUnit/load/) to `QUnit.start()`, to fix a deprecation
8 | warning in QUnit 2.21 and later. (Timo Tijhof) [77f00c47a4](https://github.com/qunitjs/node-qunit/commit/77f00c47a4c40677de2f6ef3d6d2e6edf47cf118)
9 |
10 | ## v2.0.0
11 |
12 | Changed:
13 |
14 | * Update from QUnit 2.1 to QUnit 2.11. See [upstream release notes](https://github.com/qunitjs/qunit/blob/2.11.2/History.md). [#138](https://github.com/qunitjs/node-qunit/issues/138)
15 |
16 | Removed:
17 |
18 | * **(SEMVER-MAJOR)** Drop support for Node.js 8 and earlier, per [Node.js LTS schedule](https://github.com/nodejs/Release). Node 10 or higher is now required. [#144](https://github.com/qunitjs/node-qunit/pull/144)
19 |
20 | ## v1.0.0
21 |
22 | This release upgrades QUnit to 2.x. See also .
23 |
24 | Changed:
25 |
26 | * **(SEMVER-MAJOR)** Update qunitjs version from 1.23.1 to 2.1.1. (Timo Tijhof)
27 | * Update istanbul from 0.2-harmony to 0.4.5. [#127](https://github.com/qunitjs/node-qunit/issues/127)
28 | * doc: Update description and add npm version badge to readme.
29 |
30 | Removed:
31 |
32 | * **(SEMVER-MAJOR)** Drop support for legacy Node.js, per [Node.js LTS schedule](https://github.com/nodejs/LTS/tree/a5b8bc19b5#readme). Node 4 or higher is now required.
33 |
34 | ## v0.9.3
35 |
36 | **Note:** The next release will drop support for QUnit 1.x. The new API already works in QUnit 1.23. Use node-qunit 0.9.3 to migrate first, for a seamless upgrade to QUnit 2 after this. See for more information.
37 |
38 | Changed:
39 |
40 | * Update qunitjs version from 1.10.0 to 1.23.1. (Yomi Osamiluyi)
41 | * tests: Use shorter timeouts to speed up the async test fixtures.
42 |
43 | ## v0.9.2
44 |
45 | Fixes:
46 |
47 | * Add support for Node 6 and Node 7. (Timo Tijhof) [#133](https://github.com/qunitjs/node-qunit/issues/133)
48 |
49 | ## v0.9.0
50 |
51 | Removed:
52 |
53 | * Removed buggy regexp option from instrument filter. (Bruno Jouhier) [#126](https://github.com/qunitjs/node-qunit/pull/126)
54 |
55 | ## v0.8.0
56 |
57 | Added:
58 | * Support instrumentation of multiple files through a new `coverage.files` option. (Bruno Jouhier) [#125](https://github.com/qunitjs/node-qunit/pull/125)
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2008-2013 Oleg Slobodskoi
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 |
6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 |
8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security policy
2 |
3 | ## Supported versions
4 |
5 | The latest release is supported with security updates.
6 |
7 | ## Reporting a vulnerability
8 |
9 | If you discover a vulnerability, we would like to know about it so we can take steps to address it as quickly as possible.
10 |
11 | E-mail your findings to security@jquery.com. Thanks!
12 |
--------------------------------------------------------------------------------
/bin/cli.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var util = require('util'),
4 | argsparser = require('argsparser'),
5 | fs = require('fs');
6 |
7 | var root = __dirname + '/..',
8 | args = argsparser.parse(),
9 | testrunner = require(root),
10 | o = testrunner.options,
11 | code, tests,
12 | help;
13 |
14 | help = ''
15 | + '\nUsage: cli [options] value (boolean value can be used)'
16 | + '\n'
17 | + '\nOptions:'
18 | + '\n -c, --code path to code you want to test'
19 | + '\n -t, --tests path to tests (space separated)'
20 | + '\n -d, --deps dependency paths - files required before code (space separated)'
21 | + '\n -l, --log logging options, json have to be used'
22 | + '\n --cov create tests coverage report'
23 | + '\n --timeout max block duration (in ms)'
24 | + '\n -h, --help show this help'
25 | + '\n -v, --version show module version'
26 | + '\n';
27 |
28 | /**
29 | * Parses a code or dependency argument, returning an object defining the
30 | * specified file path or/and module name.
31 | * The exports of the module will be exposed globally by default. To expose
32 | * exports as a named variable, prefix the resource with the desired variable
33 | * name followed by a colon.
34 | * This allows you to more accurately recreate browser usage of QUnit, for
35 | * tests which are portable between browser runtime environmemts and Node.js.
36 | * @param {string} path to file or module name to require.
37 | * @return {Object} resource
38 | */
39 | function parsePath(path) {
40 | var parts = path.split(':'),
41 | resource = {
42 | path: path
43 | };
44 |
45 | if (parts.length === 2) {
46 | resource.namespace = parts[0];
47 | resource.path = parts[1];
48 | }
49 |
50 | return resource;
51 | }
52 |
53 | for (var key in args) {
54 | switch(key) {
55 | case 'node':
56 | // Skip the 'node' argument
57 | break;
58 | case '-c':
59 | case '--code':
60 | code = parsePath(args[key]);
61 | break;
62 | case '-t':
63 | case '--tests':
64 | // it's assumed that tests arguments will be file paths whose
65 | // contents are to be made global. This is consistent with use
66 | // of QUnit in browsers.
67 | tests = args[key];
68 | break;
69 | case '-d':
70 | case '--deps':
71 | o.deps = args[key];
72 | if (!Array.isArray(o.deps)) {
73 | o.deps = [o.deps];
74 | }
75 | o.deps = o.deps.map(parsePath);
76 | break;
77 | case '-l':
78 | case '--log':
79 | eval('o.log = ' + args[key]);
80 | break;
81 | case '--cov':
82 | o.coverage = args[key];
83 | break;
84 | case '-p':
85 | case '--paths':
86 | o.paths = args[key];
87 | break;
88 | case '--timeout':
89 | o.maxBlockDuration = args[key];
90 | break;
91 | case '-v':
92 | case '--version':
93 | util.print(
94 | JSON.parse(
95 | fs.readFileSync(__dirname + '/../package.json')
96 | ).version + '\n'
97 | );
98 | return;
99 | case '-h':
100 | case '-?':
101 | case '--help':
102 | util.print(help);
103 | return;
104 | }
105 | }
106 | if(!code || !tests) {
107 | util.print(help);
108 | util.print('\nBoth --code and --tests arguments are required\n');
109 | return;
110 | }
111 |
112 | testrunner.run({ code: code, tests: tests, deps: o.deps, log: o.log }, function(err, stats) {
113 | if (err) {
114 | console.error(err);
115 | process.exit(1);
116 | return;
117 | }
118 |
119 | process.exit(stats.failed > 0 ? 1 : 0);
120 | });
121 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require("./lib/testrunner");
2 |
--------------------------------------------------------------------------------
/lib/child.js:
--------------------------------------------------------------------------------
1 | var QUnit = require('qunit'),
2 | path = require('path'),
3 | _ = require('underscore'),
4 | trace = require('tracejs').trace,
5 | coverage = require('./coverage'),
6 | generators = require('./generators'),
7 | co = require('co');
8 |
9 | // cycle.js: This file contains two functions, JSON.decycle and JSON.retrocycle,
10 | // which make it possible to encode cyclical structures and dags in JSON, and to
11 | // then recover them. JSONPath is used to represent the links.
12 | // http://GOESSNER.net/articles/JsonPath/
13 | require('../support/json/cycle');
14 |
15 | var options = JSON.parse(process.argv.pop()),
16 | currentModule = path.basename(options.code.path, '.js');
17 |
18 | // send ping messages to when child is blocked.
19 | // after I sent the first ping, testrunner will start to except the next ping
20 | // within maxBlockDuration, otherwise this process will be killed
21 | process.send({event: 'ping'});
22 | setInterval(function() {
23 | process.send({event: 'ping'});
24 | }, Math.floor(options.maxBlockDuration / 2));
25 |
26 | process.on('uncaughtException', function(err) {
27 | if (QUnit.config.current) {
28 | QUnit.assert.ok(false, 'Test threw unexpected exception: ' + err.message);
29 | }
30 | process.send({
31 | event: 'uncaughtException',
32 | data: {
33 | message: err.message,
34 | stack: err.stack
35 | }
36 | });
37 | });
38 |
39 | // make qunit api global, like it is in the browser
40 | _.extend(global, QUnit);
41 |
42 | // as well as the QUnit variable itself
43 | global.QUnit = QUnit;
44 |
45 | /**
46 | * Require a resource.
47 | * @param {Object} res
48 | */
49 | function _require(res, addToGlobal) {
50 | var exports = require(res.path.replace(/\.js$/, ''));
51 |
52 | if (addToGlobal) {
53 | // resource can define 'namespace' to expose its exports as a named object
54 | if (res.namespace) {
55 | global[res.namespace] = exports;
56 | } else {
57 | _.extend(global, exports);
58 | }
59 | }
60 | }
61 |
62 | /**
63 | * Callback for each started test.
64 | * @param {Object} test
65 | */
66 | QUnit.testStart(function(test) {
67 | // use last module name if no module name defined
68 | currentModule = test.module || currentModule;
69 | });
70 |
71 | /**
72 | * Callback for each assertion.
73 | * @param {Object} data
74 | */
75 | QUnit.log(function(data) {
76 | data.test = QUnit.config.current.testName;
77 | data.module = currentModule;
78 | process.send({
79 | event: 'assertionDone',
80 | data: JSON.decycle(data)
81 | });
82 | });
83 |
84 | /**
85 | * Callback for one done test.
86 | * @param {Object} test
87 | */
88 | QUnit.testDone(function(data) {
89 | // use last module name if no module name defined
90 | data.module = data.module || currentModule;
91 | process.send({
92 | event: 'testDone',
93 | data: data
94 | });
95 | });
96 |
97 | /**
98 | * Callback for all done tests in the file.
99 | * @param {Object} res
100 | */
101 | QUnit.done(_.debounce(function(data) {
102 | data.coverage = global.__coverage__;
103 |
104 | process.send({
105 | event: 'done',
106 | data: data
107 | });
108 | }, 1000));
109 |
110 | var test = QUnit.test;
111 |
112 | /**
113 | * Support generators.
114 | */
115 | global.test = QUnit.test = function(testName, callback) {
116 | var fn;
117 |
118 | if (generators.isGeneratorFn(callback)) {
119 | fn = function(assert) {
120 | var done = assert.async();
121 | co.wrap(callback).call(this, assert).then(function() {
122 | done();
123 | }).catch(function (err) {
124 | console.log(err.stack);
125 | done();
126 | });
127 | };
128 | } else {
129 | fn = callback;
130 | }
131 |
132 | return test.call(this, testName, fn);
133 | };
134 |
135 | /**
136 | * Provide better stack traces
137 | */
138 | var error = console.error;
139 | console.error = function(obj) {
140 | // log full stacktrace
141 | if (obj && obj.stack) {
142 | obj = trace(obj);
143 | }
144 |
145 | return error.apply(this, arguments);
146 | };
147 |
148 | if (options.coverage) {
149 | coverage.instrument(options);
150 | }
151 |
152 | // require deps
153 | options.deps.forEach(function(dep) {
154 | _require(dep, true);
155 | });
156 |
157 | // require code
158 | _require(options.code, true);
159 |
160 | // require tests
161 | options.tests.forEach(function(test) {
162 | _require(test, false);
163 | });
164 |
165 | QUnit.start();
166 |
--------------------------------------------------------------------------------
/lib/coverage.js:
--------------------------------------------------------------------------------
1 | var path = require('path'),
2 | _ = require('underscore');
3 |
4 | var istanbul,
5 | collector,
6 | options = {
7 | dir: 'coverage',
8 | reporters: ['lcov', 'json']
9 | };
10 |
11 | try {
12 | istanbul = require('istanbul');
13 | } catch (e) {
14 | // Ignore
15 | }
16 |
17 | exports.setup = function(opts) {
18 | collector = new istanbul.Collector();
19 |
20 | _.extend(options, opts);
21 | options.dir = path.resolve(options.dir);
22 | };
23 |
24 | exports.add = function(coverage) {
25 | if (collector && coverage) collector.add(coverage);
26 | };
27 |
28 | exports.get = function() {
29 | var summaries;
30 | if (collector) {
31 | summaries = [];
32 | collector.files().forEach(function(file) {
33 | summaries.push(istanbul.utils.summarizeFileCoverage(collector.fileCoverageFor(file)));
34 | });
35 | return istanbul.utils.mergeSummaryObjects.apply(null, summaries);
36 | }
37 | };
38 |
39 | exports.report = function() {
40 | var Report, reports;
41 |
42 | if (collector) {
43 | Report = istanbul.Report;
44 |
45 | reports = options.reporters.map(function (report) {
46 | return Report.create(report, options);
47 | });
48 |
49 | reports.forEach(function(rep) {
50 | rep.writeReport(collector, true);
51 | });
52 | }
53 | };
54 |
55 | exports.instrument = function(options) {
56 | var matcher, instrumenter;
57 |
58 | matcher = function (file) {
59 | var files = options.coverage.files;
60 | if (files) {
61 | files = Array.isArray(files) ? files : [files];
62 | return files.some(function(f) {
63 | if (typeof f === 'string') return file.indexOf(f) === 0;
64 | else throw new Error("invalid entry in options.coverage.files: " + typeof f);
65 | });
66 | } else {
67 | return file === options.code.path;
68 | }
69 | }
70 | instrumenter = new istanbul.Instrumenter();
71 | istanbul.hook.hookRequire(matcher, instrumenter.instrumentSync.bind(instrumenter));
72 | };
73 |
74 | if (!istanbul) {
75 | _.each(exports, function(fn, name) {
76 | exports[name] = function() {
77 | console.error('\nModule "istanbul" is not installed.'.red);
78 | process.exit(1);
79 | };
80 | });
81 | }
82 |
--------------------------------------------------------------------------------
/lib/generators.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /**
4 | * Is true when generators are supported.
5 | *
6 | * @deprecated since node-qunit 2.0.0: This is always true.
7 | */
8 | exports.support = true;
9 |
10 | /**
11 | * Returns true if function is a generator fn.
12 | *
13 | * @param {Function} fn
14 | * @return {Boolean}
15 | */
16 | exports.isGeneratorFn = function(fn) {
17 | return fn.constructor.name == 'GeneratorFunction';
18 | }
19 |
--------------------------------------------------------------------------------
/lib/log.js:
--------------------------------------------------------------------------------
1 | var Table = require('cli-table');
2 |
3 | var data,
4 | log = console.log,
5 | fileColWidth = 50;
6 |
7 | data = {
8 | assertions: [],
9 | tests: [],
10 | summaries: [],
11 | coverages: []
12 | };
13 |
14 | /**
15 | * Add data to the log report.
16 | *
17 | * @param {String} type
18 | * @param {Object} obj
19 | * @return {Array}
20 | */
21 | exports.add = function(type, obj) {
22 | if (obj) {
23 | data[type].push(obj);
24 | }
25 | return data[type];
26 | };
27 |
28 | /**
29 | * Get global tests stats in unified format
30 | */
31 | exports.stats = function() {
32 | var stats = {
33 | files: 0,
34 | assertions: 0,
35 | failed: 0,
36 | passed: 0,
37 | runtime: 0
38 | };
39 |
40 | data.summaries.forEach(function(file) {
41 | stats.files++;
42 | stats.assertions += file.total;
43 | stats.failed += file.failed;
44 | stats.passed += file.passed;
45 | stats.runtime += file.runtime;
46 | });
47 |
48 | stats.tests = data.tests.length;
49 |
50 | stats.coverage = {
51 | files: 0,
52 | statements: { covered: 0, total: 0 },
53 | branches: { covered: 0, total: 0 },
54 | functions: { covered: 0, total: 0 },
55 | lines: { covered: 0, total: 0 }
56 | };
57 |
58 | data.coverages.forEach(function(file) {
59 | stats.coverage.files++;
60 | stats.coverage.statements.covered += file.statements.covered;
61 | stats.coverage.statements.total += file.statements.total;
62 | stats.coverage.branches.covered += file.branches.covered;
63 | stats.coverage.branches.total += file.branches.total;
64 | stats.coverage.functions.covered += file.functions.covered;
65 | stats.coverage.functions.total += file.functions.total;
66 | stats.coverage.lines.covered += file.lines.covered;
67 | stats.coverage.lines.total += file.lines.total;
68 | });
69 |
70 | return stats;
71 | };
72 |
73 | /**
74 | * Reset global stats data
75 | */
76 | exports.reset = function() {
77 | data = {
78 | assertions: [],
79 | tests: [],
80 | summaries: [],
81 | coverages: []
82 | };
83 | };
84 |
85 | var print = exports.print = {};
86 |
87 | print.assertions = function() {
88 | var table,
89 | currentModule, module,
90 | currentTest, test;
91 |
92 | table = new Table({
93 | head: ['Module', 'Test', 'Assertion', 'Result']
94 | });
95 |
96 | data.assertions.forEach(function(data) {
97 | // just easier to read the table
98 | if (data.module === currentModule) {
99 | module = '';
100 | } else {
101 | module = currentModule = data.module;
102 | }
103 |
104 | // just easier to read the table
105 | if (data.test === currentTest) {
106 | test = '';
107 | } else {
108 | test = currentTest = data.test;
109 | }
110 |
111 | table.push([module, test, data.message || '', data.result ? 'ok' : 'fail']);
112 | });
113 |
114 | log('\nAssertions:\n' + table.toString());
115 | };
116 |
117 | print.errors = function() {
118 | var errors = [];
119 |
120 | data.assertions.forEach(function(data) {
121 | if (!data.result) {
122 | errors.push(data);
123 | }
124 | });
125 |
126 | if (errors.length) {
127 | log('\n\nErrors:');
128 | errors.forEach(function(data) {
129 | log('\nModule: ' + data.module + ' Test: ' + data.test);
130 | if (data.message) {
131 | log(data.message);
132 | }
133 |
134 | if (data.source) {
135 | log(data.source);
136 | }
137 |
138 | if (data.expected != null || data.actual != null) {
139 | //it will be an error if data.expected !== data.actual, but if they're
140 | //both undefined, it means that they were just not filled out because
141 | //no assertions were hit (likely due to code error that would have been logged as source or message).
142 | log('Actual value:');
143 | log(data.actual);
144 | log('Expected value:');
145 | log(data.expected);
146 | }
147 | });
148 | }
149 | };
150 |
151 | print.tests = function() {
152 | var table,
153 | currentModule, module;
154 |
155 | table = new Table({
156 | head: ['Module', 'Test', 'Failed', 'Passed', 'Total']
157 | });
158 |
159 | data.tests.forEach(function(data) {
160 | // just easier to read the table
161 | if (data.module === currentModule) {
162 | module = '';
163 | } else {
164 | module = currentModule = data.module;
165 | }
166 |
167 | table.push([module, data.name, data.failed, data.passed, data.total]);
168 | });
169 |
170 | log('\nTests:\n' + table.toString());
171 | };
172 |
173 | // truncate file name
174 | function truncFile(code) {
175 | if (code && code.length > fileColWidth) {
176 | code = '...' + code.slice(code.length - fileColWidth + 3);
177 | }
178 | return code;
179 | }
180 |
181 | print.summary = function() {
182 | var table;
183 |
184 | table = new Table({
185 | head: ['File', 'Failed', 'Passed', 'Total', 'Runtime']
186 | });
187 |
188 | data.summaries.forEach(function(data) {
189 | table.push([truncFile(data.code), data.failed, data.passed, data.total, data.runtime]);
190 | });
191 |
192 | log('\nSummary:\n' + table.toString());
193 | };
194 |
195 | print.globalSummary = function() {
196 | var table,
197 | data = exports.stats();
198 |
199 | table = new Table({
200 | head: ['Files', 'Tests', 'Assertions', 'Failed', 'Passed', 'Runtime']
201 | });
202 |
203 | table.push([data.files, data.tests, data.assertions, data.failed,
204 | data.passed, data.runtime]);
205 |
206 | log('\nGlobal summary:\n' + table.toString());
207 | };
208 |
209 | function getMet(metric) {
210 | function percent(covered, total) {
211 | var tmp;
212 | if (total > 0) {
213 | tmp = 1000 * 100 * covered / total + 5;
214 | return Math.floor(tmp / 10) / 100;
215 | } else {
216 | return 100.00;
217 | }
218 | }
219 | if (!metric.pct) metric.pct = percent(metric.covered, metric.total);
220 | return metric.pct + '% (' + metric.covered + '/' + metric.total + ')';
221 | }
222 |
223 | print.coverage = function() {
224 | var table;
225 |
226 | if (!data.coverages.length) return;
227 |
228 | table = new Table({
229 | head: ['File', 'Statements', 'Branches', 'Functions', 'Lines']
230 | });
231 |
232 | data.coverages.forEach(function(coverage) {
233 | table.push([
234 | truncFile(coverage.code),
235 | getMet(coverage.statements),
236 | getMet(coverage.branches),
237 | getMet(coverage.functions),
238 | getMet(coverage.lines)]);
239 | });
240 |
241 | log('\nCoverage:\n' + table.toString());
242 | };
243 |
244 | print.globalCoverage = function() {
245 | var coverage, table;
246 |
247 | if (!data.coverages.length) return;
248 |
249 | coverage = exports.stats().coverage;
250 | table = new Table({
251 | head: ['Files', 'Statements', 'Branches', 'Functions', 'Lines']
252 | });
253 |
254 | table.push([
255 | coverage.files,
256 | getMet(coverage.statements),
257 | getMet(coverage.branches),
258 | getMet(coverage.functions),
259 | getMet(coverage.lines)
260 | ]);
261 |
262 | log('\nGlobal coverage:\n' + table.toString());
263 | };
264 |
--------------------------------------------------------------------------------
/lib/testrunner.js:
--------------------------------------------------------------------------------
1 | var path = require('path'),
2 | coverage = require('./coverage'),
3 | cp = require('child_process'),
4 | _ = require('underscore'),
5 | log = exports.log = require('./log');
6 |
7 | var options,
8 | noop = function() {};
9 |
10 | options = exports.options = {
11 |
12 | // logging options
13 | log: {
14 |
15 | // log assertions overview
16 | assertions: true,
17 |
18 | // log expected and actual values for failed tests
19 | errors: true,
20 |
21 | // log tests overview
22 | tests: true,
23 |
24 | // log summary
25 | summary: true,
26 |
27 | // log global summary (all files)
28 | globalSummary: true,
29 |
30 | // log coverage
31 | coverage: true,
32 |
33 | // log global coverage (all files)
34 | globalCoverage: true,
35 |
36 | // log currently testing code file
37 | testing: true
38 | },
39 |
40 | // run test coverage tool
41 | coverage: false,
42 |
43 | // define dependencies, which are required then before code
44 | deps: null,
45 |
46 | // define namespace your code will be attached to on global['your namespace']
47 | namespace: null,
48 |
49 | // max amount of ms child can be blocked, after that we assume running an infinite loop
50 | maxBlockDuration: 2000
51 | };
52 |
53 | /**
54 | * Run one spawned instance with tests
55 | * @param {Object} opts
56 | * @param {Function} callback
57 | */
58 | function runOne(opts, callback) {
59 | var child;
60 | var pingCheckTimeoutId;
61 | var argv = process.argv.slice();
62 |
63 | argv.push(JSON.stringify(opts));
64 | child = cp.fork(__dirname + '/child.js', argv, {env: process.env});
65 |
66 | function kill() {
67 | process.removeListener('exit', kill);
68 | child.kill();
69 | }
70 |
71 | function complete(err, data) {
72 | kill();
73 | clearTimeout(pingCheckTimeoutId);
74 | callback(err, data)
75 | }
76 |
77 | child.on('message', function(msg) {
78 | switch (msg.event) {
79 | case 'ping':
80 | clearTimeout(pingCheckTimeoutId);
81 | pingCheckTimeoutId = setTimeout(function() {
82 | complete(new Error('Process blocked for too long'));
83 | }, opts.maxBlockDuration);
84 | break;
85 | case 'assertionDone':
86 | log.add('assertions', msg.data);
87 | break;
88 | case 'testDone':
89 | log.add('tests', msg.data);
90 | break;
91 | case 'done':
92 | clearTimeout(pingCheckTimeoutId);
93 | msg.data.code = opts.code.path;
94 | log.add('summaries', msg.data);
95 | if (opts.coverage) {
96 | coverage.add(msg.data.coverage);
97 | msg.data.coverage = coverage.get();
98 | msg.data.coverage.code = msg.data.code;
99 | log.add('coverages', msg.data.coverage);
100 | }
101 | if (opts.log.testing) {
102 | console.log('done');
103 | }
104 | complete(null, msg.data);
105 | break;
106 | case 'uncaughtException':
107 | complete(_.extend(new Error(), msg.data));
108 | break;
109 | }
110 | });
111 |
112 | process.on('exit', kill);
113 |
114 | if (opts.log.testing) {
115 | console.log('\nTesting ', opts.code.path + ' ... ');
116 | }
117 | }
118 |
119 | /**
120 | * Make an absolute path from relative
121 | * @param {string|Object} file
122 | * @return {Object}
123 | */
124 | function absPath(file) {
125 | if (typeof file === 'string') {
126 | file = {path: file};
127 | }
128 |
129 | if (file.path.charAt(0) != '/') {
130 | file.path = path.resolve(process.cwd(), file.path);
131 | }
132 |
133 | return file;
134 | }
135 |
136 | /**
137 | * Convert path or array of paths to array of abs paths
138 | * @param {Array|string} files
139 | * @return {Array}
140 | */
141 | function absPaths(files) {
142 | var ret = [];
143 |
144 | if (Array.isArray(files)) {
145 | files.forEach(function(file) {
146 | ret.push(absPath(file));
147 | });
148 | } else if (files) {
149 | ret.push(absPath(files));
150 | }
151 |
152 | return ret;
153 | }
154 |
155 | /**
156 | * Run tests in spawned node instance async for every test.
157 | * @param {Object|Array} files
158 | * @param {Function} callback optional
159 | */
160 | exports.run = function(files, callback) {
161 | var filesCount = 0;
162 |
163 | callback || (callback = noop);
164 |
165 | if (!Array.isArray(files)) {
166 | files = [files];
167 | }
168 |
169 | if (options.coverage || files[0].coverage) coverage.setup(options.coverage);
170 |
171 | files.forEach(function(file) {
172 | var opts = _.extend({}, options, file);
173 |
174 | !opts.log && (opts.log = {});
175 | opts.deps = absPaths(opts.deps);
176 | opts.code = absPath(opts.code);
177 | opts.tests = absPaths(opts.tests);
178 |
179 | runOne(opts, function(err) {
180 | if (err) {
181 | return callback(err, log.stats());
182 | }
183 |
184 | filesCount++;
185 |
186 | if (filesCount >= files.length) {
187 | _.each(opts.log, function(val, name) {
188 | if (val && log.print[name]) {
189 | log.print[name]();
190 | }
191 | });
192 |
193 | // Write coverage report.
194 | if (opts.coverage) coverage.report();
195 | callback(null, log.stats());
196 | }
197 | });
198 | });
199 | };
200 |
201 |
202 | /**
203 | * Set options
204 | * @param {Object}
205 | */
206 | exports.setup = function(opts) {
207 | _.extend(options, opts);
208 | };
209 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-qunit",
3 | "description": "QUnit testing framework for Node.js",
4 | "version": "2.0.1",
5 | "author": "Oleg Slobodskoi ",
6 | "contributors": [
7 | {
8 | "name": "Jonathan Buchanan"
9 | },
10 | {
11 | "name": "Ashar Voultoiz"
12 | },
13 | {
14 | "name": "Drew Fyock"
15 | },
16 | {
17 | "name": "Timo Tijhof"
18 | }
19 | ],
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/qunitjs/node-qunit.git"
23 | },
24 | "license": "MIT",
25 | "keywords": [
26 | "TDD",
27 | "QUnit",
28 | "unit",
29 | "testing",
30 | "tests",
31 | "async"
32 | ],
33 | "bin": {
34 | "qunit": "./bin/cli.js"
35 | },
36 | "engines": {
37 | "node": ">=10"
38 | },
39 | "scripts": {
40 | "test": "node test/testrunner.js && eslint .",
41 | "lint": "eslint ."
42 | },
43 | "dependencies": {
44 | "argsparser": "^0.0.7",
45 | "cli-table": "^0.3.1",
46 | "co": "^4.6.0",
47 | "qunit": "^2.11.2",
48 | "tracejs": "^0.1.8",
49 | "underscore": "^1.11.0"
50 | },
51 | "devDependencies": {
52 | "chainer": "^0.0.5",
53 | "eslint": "^7.8.1",
54 | "timekeeper": "^2.0.0"
55 | },
56 | "optionalDependencies": {
57 | "istanbul": "0.4.5"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | [](https://www.npmjs.com/package/node-qunit)
2 |
3 | ## QUnit testing framework for Node.js
4 |
5 | https://qunitjs.com
6 |
7 | https://github.com/qunitjs/qunit
8 |
9 | ### Features
10 |
11 | - cli
12 | - testrunner api
13 | - test coverage via istanbul
14 | - tests inside of one testfile run synchronous, but every testfile runs parallel
15 | - tests from each file run in its own spawned node process
16 | - same API for client and server side code (original QUnit is used)
17 | - the simplest API of the world, especially for asynchronous testing
18 | - you can write tests in TDD or BDD style depending on your task and test type
19 | - you can run the same tests in browser if there is no dependencies to node
20 | - generators support
21 |
22 | ### Installation
23 |
24 | ```bash
25 | $ npm i node-qunit
26 | ```
27 |
28 | #### Package Name Up to 1.0.0
29 |
30 | Up until version 1.0.0, this package was published under the name `qunit`. That
31 | name is now used by the official QUnit package and CLI, and this package will be
32 | published as `node-qunit` from version 1.0.0 onward.
33 |
34 | Additionally, prior to 1.0.0, the `node-qunit` package was a different project
35 | that was deprecated and now lives under the name [`qnit`](https://www.npmjs.com/package/qnit).
36 |
37 | ### API
38 |
39 | https://api.qunitjs.com
40 |
41 | #### The only exception
42 |
43 | ```javascript
44 | // Separate tests into modules.
45 | // Use `QUnit` namespace, because `module` is reserved for node.
46 | QUnit.module(name, lifecycle)
47 | ```
48 |
49 | ### Usage
50 |
51 | #### Command line
52 |
53 | Read full cli api doc using "--help" or "-h":
54 |
55 | ```bash
56 | $ qunit -h
57 |
58 | $ qunit -c ./code.js -t ./tests.js
59 | ```
60 |
61 | By default, code and dependencies are added to the global scope. To specify
62 | requiring them into a namespace object, prefix the path or module name with the
63 | variable name to be used for the namespace object, followed by a colon:
64 |
65 | ```bash
66 | $ qunit -c code:./code.js -d utils:utilmodule -t ./time.js
67 | ```
68 |
69 | #### via api
70 |
71 | ```javascript
72 | var testrunner = require("node-qunit");
73 |
74 | // Defaults:
75 | {
76 | // logging options
77 | log: {
78 |
79 | // log assertions overview
80 | assertions: true,
81 |
82 | // log expected and actual values for failed tests
83 | errors: true,
84 |
85 | // log tests overview
86 | tests: true,
87 |
88 | // log summary
89 | summary: true,
90 |
91 | // log global summary (all files)
92 | globalSummary: true,
93 |
94 | // log coverage
95 | coverage: true,
96 |
97 | // log global coverage (all files)
98 | globalCoverage: true,
99 |
100 | // log currently testing code file
101 | testing: true
102 | },
103 |
104 | // run test coverage tool
105 | coverage: false,
106 |
107 | // define dependencies, which are required then before code
108 | deps: null,
109 |
110 | // define namespace your code will be attached to on global['your namespace']
111 | namespace: null,
112 |
113 | // max amount of ms child can be blocked, after that we assume running an infinite loop
114 | maxBlockDuration: 2000
115 | }
116 | ```
117 |
118 | ```javascript
119 | // change any option for all tests globally
120 | testrunner.options.optionName = value;
121 |
122 | // or use setup function
123 | testrunner.setup({
124 | log: {
125 | summary: true
126 | }
127 | });
128 |
129 |
130 | // one code and tests file
131 | testrunner.run({
132 | code: "/path/to/your/code.js",
133 | tests: "/path/to/your/tests.js"
134 | }, callback);
135 |
136 | // require code into a namespace object, rather than globally
137 | testrunner.run({
138 | code: {path: "/path/to/your/code.js", namespace: "code"},
139 | tests: "/path/to/your/tests.js"
140 | }, callback);
141 |
142 | // one code and multiple tests file
143 | testrunner.run({
144 | code: "/path/to/your/code.js",
145 | tests: ["/path/to/your/tests.js", "/path/to/your/tests1.js"]
146 | }, callback);
147 |
148 | // array of code and test files
149 | testrunner.run([
150 | {
151 | code: "/path/to/your/code.js",
152 | tests: "/path/to/your/tests.js"
153 | },
154 | {
155 | code: "/path/to/your/code.js",
156 | tests: "/path/to/your/tests.js"
157 | }
158 | ], callback);
159 |
160 | // using testrunner callback
161 | testrunner.run({
162 | code: "/path/to/your/code.js",
163 | tests: "/path/to/your/tests.js"
164 | }, function(err, report) {
165 | console.dir(report);
166 | });
167 |
168 | // specify dependency
169 | testrunner.run({
170 | deps: "/path/to/your/dependency.js",
171 | code: "/path/to/your/code.js",
172 | tests: "/path/to/your/tests.js"
173 | }, callback);
174 |
175 | // dependencies can be modules or files
176 | testrunner.run({
177 | deps: "modulename",
178 | code: "/path/to/your/code.js",
179 | tests: "/path/to/your/tests.js"
180 | }, callback);
181 |
182 | // dependencies can required into a namespace object
183 | testrunner.run({
184 | deps: {path: "utilmodule", namespace: "utils"},
185 | code: "/path/to/your/code.js",
186 | tests: "/path/to/your/tests.js"
187 | }, callback);
188 |
189 | // specify multiple dependencies
190 | testrunner.run({
191 | deps: ["/path/to/your/dependency1.js", "/path/to/your/dependency2.js"],
192 | code: "/path/to/your/code.js",
193 | tests: "/path/to/your/tests.js"
194 | }, callback);
195 | ```
196 |
197 | ### Writing tests
198 |
199 | QUnit API and code which have to be tested are already loaded and attached to the global context.
200 |
201 | Some tests examples
202 |
203 | ```javascript
204 | test("a basic test example", function (assert) {
205 | assert.ok(true, "this test is fine");
206 | var value = "hello";
207 | assert.equal("hello", value, "We expect value to be hello");
208 | });
209 |
210 | QUnit.module("Module A");
211 |
212 | test("first test within module", function (assert) {
213 | assert.ok(true, "a dummy");
214 | });
215 |
216 | test("second test within module", function (assert) {
217 | assert.ok(true, "dummy 1 of 2");
218 | assert.ok(true, "dummy 2 of 2");
219 | });
220 |
221 | QUnit.module("Module B", {
222 | setup: function () {
223 | // do some initial stuff before every test for this module
224 | },
225 | teardown: function () {
226 | // do some stuff after every test for this module
227 | }
228 | });
229 |
230 | test("some other test", function (assert) {
231 | assert.expect(2);
232 | assert.equal(true, false, "failing test");
233 | assert.equal(true, true, "passing test");
234 | });
235 |
236 | QUnit.module("Module C", {
237 | setup: function() {
238 | // setup a shared environment for each test
239 | this.options = { test: 123 };
240 | }
241 | });
242 |
243 | test("this test is using shared environment", function (assert) {
244 | assert.deepEqual({ test: 123 }, this.options, "passing test");
245 | });
246 |
247 | test("this is an async test example", function (assert) {
248 | var done = assert.stop();
249 | assert.expect(2);
250 | setTimeout(function () {
251 | assert.ok(true, "finished async test");
252 | assert.strictEqual(true, true, "Strict equal assertion uses ===");
253 | done();
254 | }, 100);
255 | });
256 | ```
257 |
258 | ### Generators support
259 |
260 | ```javascript
261 | test("my async test with generators", function* (assert) {
262 | var data = yield asyncFn();
263 | assert.equal(data, {a: 1}, 'generators work');
264 | });
265 | ```
266 |
267 | ### Run tests
268 |
269 | ```bash
270 | $ npm it
271 | ```
272 |
273 | ### Coverage
274 |
275 | Code coverage via Istanbul.
276 |
277 | To utilize, install `istanbul` and set option `coverage: true` or give a path where to store report `coverage: {dir: "coverage/path"}` or pass `--cov` parameter in the shell.
278 |
279 | To specify the format of coverage report pass reporters array to the coverage options: `coverage: {reporters: ['lcov', 'json']}` (default)
280 |
281 | Coverage calculations based on code and tests passed to `node-qunit`.
282 |
--------------------------------------------------------------------------------
/support/json/cycle.js:
--------------------------------------------------------------------------------
1 | // cycle.js
2 | // 2011-08-24
3 |
4 | /*jslint evil: true, regexp: true */
5 |
6 | /*members $ref, apply, call, decycle, hasOwnProperty, length, prototype, push,
7 | retrocycle, stringify, test, toString
8 | */
9 |
10 | if (typeof JSON.decycle !== 'function') {
11 | JSON.decycle = function decycle(object) {
12 | 'use strict';
13 |
14 | // Make a deep copy of an object or array, assuring that there is at most
15 | // one instance of each object or array in the resulting structure. The
16 | // duplicate references (which might be forming cycles) are replaced with
17 | // an object of the form
18 | // {$ref: PATH}
19 | // where the PATH is a JSONPath string that locates the first occurance.
20 | // So,
21 | // var a = [];
22 | // a[0] = a;
23 | // return JSON.stringify(JSON.decycle(a));
24 | // produces the string '[{"$ref":"$"}]'.
25 |
26 | // JSONPath is used to locate the unique object. $ indicates the top level of
27 | // the object or array. [NUMBER] or [STRING] indicates a child member or
28 | // property.
29 |
30 | var objects = [], // Keep a reference to each unique object or array
31 | paths = []; // Keep the path to each unique object or array
32 |
33 | return (function derez(value, path) {
34 |
35 | // The derez recurses through the object, producing the deep copy.
36 |
37 | var i, // The loop counter
38 | name, // Property name
39 | nu; // The new object or array
40 |
41 | switch (typeof value) {
42 | case 'object':
43 |
44 | // typeof null === 'object', so get out if this value is not really an object.
45 |
46 | if (!value) {
47 | return null;
48 | }
49 |
50 | // If the value is an object or array, look to see if we have already
51 | // encountered it. If so, return a $ref/path object. This is a hard way,
52 | // linear search that will get slower as the number of unique objects grows.
53 |
54 | for (i = 0; i < objects.length; i += 1) {
55 | if (objects[i] === value) {
56 | return {$ref: paths[i]};
57 | }
58 | }
59 |
60 | // Otherwise, accumulate the unique value and its path.
61 |
62 | objects.push(value);
63 | paths.push(path);
64 |
65 | // If it is an array, replicate the array.
66 |
67 | if (Object.prototype.toString.apply(value) === '[object Array]') {
68 | nu = [];
69 | for (i = 0; i < value.length; i += 1) {
70 | nu[i] = derez(value[i], path + '[' + i + ']');
71 | }
72 | } else {
73 |
74 | // If it is an object, replicate the object.
75 |
76 | nu = {};
77 | for (name in value) {
78 | if (Object.prototype.hasOwnProperty.call(value, name)) {
79 | nu[name] = derez(value[name],
80 | path + '[' + JSON.stringify(name) + ']');
81 | }
82 | }
83 | }
84 | return nu;
85 | case 'number':
86 | case 'string':
87 | case 'boolean':
88 | return value;
89 | }
90 | }(object, '$'));
91 | };
92 | }
93 |
94 |
95 | if (typeof JSON.retrocycle !== 'function') {
96 | JSON.retrocycle = function retrocycle($) {
97 | 'use strict';
98 |
99 | // Restore an object that was reduced by decycle. Members whose values are
100 | // objects of the form
101 | // {$ref: PATH}
102 | // are replaced with references to the value found by the PATH. This will
103 | // restore cycles. The object will be mutated.
104 |
105 | // The eval function is used to locate the values described by a PATH. The
106 | // root object is kept in a $ variable. A regular expression is used to
107 | // assure that the PATH is extremely well formed. The regexp contains nested
108 | // * quantifiers. That has been known to have extremely bad performance
109 | // problems on some browsers for very long strings. A PATH is expected to be
110 | // reasonably short. A PATH is allowed to belong to a very restricted subset of
111 | // Goessner's JSONPath.
112 |
113 | // So,
114 | // var s = '[{"$ref":"$"}]';
115 | // return JSON.retrocycle(JSON.parse(s));
116 | // produces an array containing a single element which is the array itself.
117 |
118 | var px =
119 | /^\$(?:\[(?:\d+|"(?:[^\\"\u0000-\u001f]|\\([\\"/bfnrt]|u[0-9a-zA-Z]{4}))*")\])*$/;
120 |
121 | (function rez(value) {
122 |
123 | // The rez function walks recursively through the object looking for $ref
124 | // properties. When it finds one that has a value that is a path, then it
125 | // replaces the $ref object with a reference to the value that is found by
126 | // the path.
127 |
128 | var i, item, name, path;
129 |
130 | if (value && typeof value === 'object') {
131 | if (Object.prototype.toString.apply(value) === '[object Array]') {
132 | for (i = 0; i < value.length; i += 1) {
133 | item = value[i];
134 | if (item && typeof item === 'object') {
135 | path = item.$ref;
136 | if (typeof path === 'string' && px.test(path)) {
137 | value[i] = eval(path);
138 | } else {
139 | rez(item);
140 | }
141 | }
142 | }
143 | } else {
144 | for (name in value) {
145 | if (typeof value[name] === 'object') {
146 | item = value[name];
147 | if (item) {
148 | path = item.$ref;
149 | if (typeof path === 'string' && px.test(path)) {
150 | value[name] = eval(path);
151 | } else {
152 | rez(item);
153 | }
154 | }
155 | }
156 | }
157 | }
158 | }
159 | }($));
160 | return $;
161 | };
162 | }
163 |
--------------------------------------------------------------------------------
/test/fixtures/async-code.js:
--------------------------------------------------------------------------------
1 | exports.blubb = 123;
2 |
--------------------------------------------------------------------------------
/test/fixtures/async-test.js:
--------------------------------------------------------------------------------
1 | test('1', function (assert){
2 | assert.ok(true, "tests intermixing sync and async tests #1");
3 | });
4 |
5 | test('a', function(assert){
6 | var done = assert.async();
7 |
8 | setTimeout(function() {
9 | assert.ok(true, 'test a1');
10 | assert.ok(true, 'test a2');
11 | done();
12 | }, 100);
13 | });
14 |
15 | test('2', function (assert){
16 | assert.ok(true, "tests intermixing sync and async tests #2");
17 | });
18 |
19 | test('b', function(assert){
20 | var done = assert.async();
21 |
22 | setTimeout(function() {
23 | assert.ok(true, 'test b1');
24 | assert.ok(true, 'test b2');
25 | done();
26 | }, 10);
27 | });
28 |
--------------------------------------------------------------------------------
/test/fixtures/child-code-global.js:
--------------------------------------------------------------------------------
1 | exports.whereFrom = function() {
2 | return "I was required as global";
3 | };
4 |
--------------------------------------------------------------------------------
/test/fixtures/child-code-namespace.js:
--------------------------------------------------------------------------------
1 | exports.whereFrom = function() {
2 | return "I was required as a namespace object";
3 | };
4 |
--------------------------------------------------------------------------------
/test/fixtures/child-tests-global.js:
--------------------------------------------------------------------------------
1 | /* global whereFrom */
2 | test("Dependency file required as global", function(assert) {
3 | assert.equal(typeof whereFrom, "function");
4 | assert.equal(whereFrom(), "I was required as global");
5 | });
6 |
--------------------------------------------------------------------------------
/test/fixtures/child-tests-namespace.js:
--------------------------------------------------------------------------------
1 | /* global testns */
2 | test("Dependency file required as a namespace object", function(assert) {
3 | assert.strictEqual(typeof testns != "undefined", true);
4 | assert.equal(typeof testns.whereFrom, "function", "right method attached to right object");
5 | assert.equal(testns.whereFrom(), "I was required as a namespace object");
6 | });
7 |
--------------------------------------------------------------------------------
/test/fixtures/coverage-code.js:
--------------------------------------------------------------------------------
1 | exports.myMethod = function() {
2 | return 123;
3 | };
4 |
5 | exports.myAsyncMethod = function(callback) {
6 | setTimeout(function() {
7 | callback(123);
8 | }, 100);
9 | };
10 |
11 | exports.myOtherMethod = function() {
12 | return 321;
13 | };
14 |
--------------------------------------------------------------------------------
/test/fixtures/coverage-multiple-code.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./coverage-code')
2 |
--------------------------------------------------------------------------------
/test/fixtures/coverage-test.js:
--------------------------------------------------------------------------------
1 | /* global myMethod, myAsyncMethod */
2 | test('myMethod test', function(assert) {
3 | assert.equal(myMethod(), 123, 'myMethod returns right result');
4 | });
5 |
6 | test('myAsyncMethod test', function(assert) {
7 | assert.ok(true, 'myAsyncMethod started');
8 |
9 | var done = assert.async();
10 | assert.expect(2);
11 |
12 | myAsyncMethod(function(data) {
13 | assert.equal(data, 123, 'myAsyncMethod returns right result');
14 | done();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/test/fixtures/generators-code.js:
--------------------------------------------------------------------------------
1 | exports.thunk = function() {
2 | return function(callback) {
3 | setTimeout(function() {
4 | callback(null, {a: 1});
5 | }, 100);
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/test/fixtures/generators-test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env es6 */
2 | /* global thunk */
3 | test('generators', function* (assert) {
4 | var data = yield thunk();
5 | assert.deepEqual(data, {a: 1}, 'works');
6 | });
7 |
--------------------------------------------------------------------------------
/test/fixtures/infinite-loop-code.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-constant-condition, no-empty
2 | while(1) {}
3 |
--------------------------------------------------------------------------------
/test/fixtures/infinite-loop-test.js:
--------------------------------------------------------------------------------
1 | test('infinite loop', function(assert) {
2 | assert.ok(true)
3 | })
4 |
--------------------------------------------------------------------------------
/test/fixtures/testrunner-code.js:
--------------------------------------------------------------------------------
1 | exports.myMethod = function() {
2 | return 123;
3 | };
4 |
5 | exports.myAsyncMethod = function(callback) {
6 | setTimeout(function() {
7 | callback(123);
8 | }, 100);
9 | };
10 |
--------------------------------------------------------------------------------
/test/fixtures/testrunner-tests.js:
--------------------------------------------------------------------------------
1 | /* global myMethod, myAsyncMethod */
2 | test('myMethod test', function(assert) {
3 | assert.equal(myMethod(), 123, 'myMethod returns right result');
4 | assert.equal(myMethod(), 321, 'this should trigger an error');
5 | });
6 |
7 | test('myAsyncMethod test', function(assert) {
8 | var done = assert.async();
9 | assert.expect(3);
10 |
11 | assert.ok(true, 'myAsyncMethod started');
12 |
13 | myAsyncMethod(function(data) {
14 | assert.equal(data, 123, 'myAsyncMethod returns right result');
15 | assert.equal(data, 321, 'this should trigger an error');
16 | done();
17 | });
18 | });
19 |
20 | test('circular reference', function(assert) {
21 | assert.equal(global, global, 'test global');
22 | });
23 |
24 | test('use original Date', function(assert) {
25 | var timekeeper = require('timekeeper');
26 |
27 | timekeeper.travel(Date.now() - 1000000);
28 |
29 | assert.ok(true, 'date modified');
30 | });
31 |
--------------------------------------------------------------------------------
/test/fixtures/uncaught-exception-code.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qunitjs/node-qunit/257cafd4e3f7a238c953d42da3633729732f5645/test/fixtures/uncaught-exception-code.js
--------------------------------------------------------------------------------
/test/fixtures/uncaught-exception-test.js:
--------------------------------------------------------------------------------
1 | throw new Error('Some error.');
2 |
--------------------------------------------------------------------------------
/test/testrunner.js:
--------------------------------------------------------------------------------
1 | var a = require('assert'),
2 | chainer = require('chainer');
3 |
4 | var tr = require('../lib/testrunner'),
5 | log = require('../lib/log'),
6 | generators = require('../lib/generators');
7 |
8 | var fixtures = __dirname + '/fixtures',
9 | chain = chainer();
10 |
11 | tr.options.log = {
12 | // log assertions overview
13 | // assertions: true,
14 | // log expected and actual values for failed tests
15 | // errors: true,
16 | // log tests overview
17 | // tests: true,
18 | // log summary
19 | // summary: true,
20 | // log global summary (all files)
21 | // globalSummary: true,
22 | // log coverage
23 | // coverage: true,
24 | // log global coverage (all files)
25 | // globalCoverage: true,
26 | // log currently testing code file
27 | testing: true
28 | };
29 |
30 | // reset log stats every time .next is called
31 | chain.next = function() {
32 | log.reset();
33 | return chainer.prototype.next.apply(this, arguments);
34 | };
35 |
36 | chain.add('base testrunner', function() {
37 | tr.run({
38 | code: fixtures + '/testrunner-code.js',
39 | tests: fixtures + '/testrunner-tests.js',
40 | }, function(err, res) {
41 | var stat = {
42 | files: 1,
43 | tests: 4,
44 | assertions: 7,
45 | failed: 2,
46 | passed: 5
47 | };
48 | a.equal(err, null, 'no errors');
49 | a.ok(res.runtime > 0, 'Date was modified');
50 | delete res.runtime;
51 | delete res.coverage;
52 | a.deepEqual(stat, res, 'base testrunner test');
53 | chain.next();
54 | });
55 | });
56 |
57 | chain.add('attach code to global', function() {
58 | tr.run({
59 | code: fixtures + '/child-code-global.js',
60 | tests: fixtures + '/child-tests-global.js',
61 | }, function(err, res) {
62 | var stat = {
63 | files: 1,
64 | tests: 1,
65 | assertions: 2,
66 | failed: 0,
67 | passed: 2
68 | };
69 |
70 | delete res.runtime;
71 | delete res.coverage;
72 | a.equal(err, null, 'no errors');
73 | a.deepEqual(stat, res, 'attaching code to global works');
74 | chain.next();
75 | });
76 | });
77 |
78 | chain.add('attach deps to global', function() {
79 | tr.run({
80 | deps: fixtures + '/child-code-global.js',
81 | code: fixtures + '/testrunner-code.js',
82 | tests: fixtures + '/child-tests-global.js',
83 | }, function(err, res) {
84 | var stat = {
85 | files: 1,
86 | tests: 1,
87 | assertions: 2,
88 | failed: 0,
89 | passed: 2
90 | };
91 |
92 | delete res.runtime;
93 | delete res.coverage;
94 | a.equal(err, null, 'no errors');
95 | a.deepEqual(stat, res, 'attaching dependencies to global works');
96 | chain.next();
97 | });
98 | });
99 |
100 | chain.add('attach code to a namespace', function() {
101 | tr.run({
102 | code: {
103 | path: fixtures + '/child-code-namespace.js',
104 | namespace: 'testns'
105 | },
106 | tests: fixtures + '/child-tests-namespace.js',
107 | }, function(err, res) {
108 | var stat = {
109 | files: 1,
110 | tests: 1,
111 | assertions: 3,
112 | failed: 0,
113 | passed: 3
114 | };
115 |
116 | delete res.runtime;
117 | delete res.coverage;
118 | a.equal(err, null, 'no errors');
119 | a.deepEqual(stat, res, 'attaching code to specified namespace works');
120 | chain.next();
121 | });
122 | });
123 |
124 | chain.add('async testing logs', function() {
125 | tr.run({
126 | code: fixtures + '/async-code.js',
127 | tests: fixtures + '/async-test.js',
128 | }, function(err, res) {
129 | var stat = {
130 | files: 1,
131 | tests: 4,
132 | assertions: 6,
133 | failed: 0,
134 | passed: 6
135 | };
136 |
137 | delete res.runtime;
138 | delete res.coverage;
139 | a.equal(err, null, 'no errors');
140 | a.deepEqual(stat, res, 'async code testing works');
141 | chain.next();
142 | });
143 | });
144 |
145 | chain.add('uncaught exception', function() {
146 | tr.run({
147 | code: fixtures + '/uncaught-exception-code.js',
148 | tests: fixtures + '/uncaught-exception-test.js',
149 | }, function(err) {
150 | a.ok(err instanceof Error, 'error was forwarded');
151 | chain.next();
152 | });
153 | });
154 |
155 | chain.add('infinite loop', function() {
156 | tr.run({
157 | code: fixtures + '/infinite-loop-code.js',
158 | tests: fixtures + '/infinite-loop-test.js',
159 | }, function(err) {
160 | a.ok(err instanceof Error, 'error was forwarded');
161 | chain.next();
162 | });
163 | });
164 |
165 | chain.add('coverage', function() {
166 | tr.options.coverage = true;
167 | tr.run({
168 | code: fixtures + '/coverage-code.js',
169 | tests: fixtures + '/coverage-test.js'
170 | }, function(err, res) {
171 | var stat = {
172 | files: 1,
173 | tests: 2,
174 | assertions: 3,
175 | failed: 0,
176 | passed: 3,
177 | coverage: {
178 | files: 1,
179 | statements: { covered: 6, total: 7 },
180 | branches: { covered: 0, total: 0 },
181 | functions: { covered: 3, total: 4 },
182 | lines: { covered: 6, total: 7 }
183 | }
184 | };
185 | delete res.runtime;
186 | a.equal(err, null, 'no errors');
187 | a.deepEqual(stat, res, 'coverage code testing works');
188 | tr.options.coverage = false;
189 | chain.next();
190 | });
191 | });
192 |
193 | chain.add('coverage-multiple', function() {
194 | tr.options.coverage = true;
195 | tr.run({
196 | code: fixtures + '/coverage-multiple-code.js',
197 | tests: fixtures + '/coverage-test.js',
198 | coverage: {
199 | files: [
200 | fixtures + '/coverage-multiple-code.js',
201 | fixtures + '/coverage-code.js'
202 | ],
203 | },
204 | }, function(err, res) {
205 | var stat = {
206 | files: 1,
207 | tests: 2,
208 | assertions: 3,
209 | failed: 0,
210 | passed: 3,
211 | coverage: {
212 | files: 1,
213 | statements: { covered: 7, total: 8 },
214 | branches: { covered: 0, total: 0 },
215 | functions: { covered: 3, total: 4 },
216 | lines: { covered: 7, total: 8 }
217 | }
218 | };
219 | delete res.runtime;
220 | a.equal(err, null, 'no errors');
221 | a.deepEqual(stat, res, 'coverage multiple code testing works');
222 | tr.options.coverage = false;
223 | chain.next();
224 | });
225 | });
226 |
227 | if (generators.support) {
228 | chain.add('generators', function() {
229 | tr.run({
230 | code: fixtures + '/generators-code.js',
231 | tests: fixtures + '/generators-test.js'
232 | }, function(err, res) {
233 | var stat = {
234 | files: 1,
235 | tests: 1,
236 | assertions: 1,
237 | failed: 0,
238 | passed: 1
239 | };
240 | delete res.runtime;
241 | delete res.coverage;
242 | a.equal(err, null, 'no errors');
243 | a.deepEqual(stat, res, 'coverage code testing works');
244 | chain.next();
245 | });
246 | });
247 | }
248 |
249 | chain.add(function() {
250 | console.log('\nAll tests ok.');
251 | });
252 |
253 | chain.start();
254 |
--------------------------------------------------------------------------------