├── .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 | [![npm](https://img.shields.io/npm/v/node-qunit.svg?style=flat)](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 | --------------------------------------------------------------------------------