UNPKG

qunit

Version:

The powerful, easy-to-use testing framework.

256 lines (213 loc) 8.21 kB
'use strict'; const path = require('path'); const url = require('url'); const requireFromCWD = require('./require-from-cwd'); const requireQUnit = require('./require-qunit'); const utils = require('./utils'); const { findReporter } = require('./find-reporter'); const DEBOUNCE_WATCH_LENGTH = 60; const DEBOUNCE_RESTART_LENGTH = 200 - DEBOUNCE_WATCH_LENGTH; const changedPendingPurge = []; let QUnit; let running = false; let restartDebounceTimer; function onUnhandledRejection (reason, _promise) { QUnit.onUncaughtException(reason); } function onUncaughtException (error, _origin) { QUnit.onUncaughtException(error); } function onExit () { if (running) { console.error('Error: Process exited before tests finished running'); const currentTest = QUnit.config.current; if (currentTest && currentTest.pauses.size > 0) { const name = currentTest.testName; console.error('Last test to run (' + name + ') has an async hold. ' + 'Ensure all assert.async() callbacks are invoked and Promises resolve. ' + 'You should also set a standard timeout via QUnit.config.testTimeout.'); } } } async function run (args, options) { // Default to non-zero exit code to avoid false positives process.exitCode = 1; const files = utils.getFilesFromArgs(args); QUnit = requireQUnit(); if (options.filter) { QUnit.config.filter = options.filter; } if (options.module) { QUnit.config.module = options.module; } const seed = options.seed; if (seed) { if (seed === 'true' || seed === true) { // NOTE: QUnit 3 will set preconfig qunit_config_seed from here. // NOTE: This duplicates logic from /src/core/config.js. Consolidated in QUnit 3. QUnit.config.seed = (Math.random().toString(36) + '0000000000').slice(2, 12); } else { QUnit.config.seed = seed; } } if (QUnit.config.seed) { console.log(`Running tests with seed: ${QUnit.config.seed}`); } // TODO: Enable mode where QUnit is not auto-injected, but other setup is // still done automatically. global.QUnit = QUnit; options.requires.forEach(requireFromCWD); findReporter(options.reporter, QUnit.reporters).init(QUnit); for (let i = 0; i < files.length; i++) { const filePath = path.resolve(process.cwd(), files[i]); delete require.cache[filePath]; // Node.js 12.0.0 has node_module_version=72 // https://nodejs.org/en/download/releases/ const nodeVint = process.config.variables.node_module_version; try { // QUnit supports passing ESM files to the 'qunit' command when used on // Node.js 12 or later. The dynamic import() keyword supports both CommonJS files // (.js, .cjs) and ESM files (.mjs), so we could simply use that unconditionally on // newer Node versions, regardless of the given file path. // // But: // - Node.js 12 emits a confusing "ExperimentalWarning" when using import(), // even if just to load a non-ESM file. So we should try to avoid it on non-ESM. // - This Node.js feature is still considered experimental so to avoid unexpected // breakage we should continue using require(). Consider flipping once stable and/or // as part of QUnit 3.0. // - Plugins and CLI bootstrap scripts may be hooking into require.extensions to modify // or transform code as it gets loaded. For compatibility with that, we should // support that until at least QUnit 3.0. // - File extensions are not sufficient to differentiate between CJS and ESM. // Use of ".mjs" is optional, as a package may configure Node to default to ESM // and optionally use ".cjs" for CJS files. // // https://nodejs.org/docs/v12.7.0/api/modules.html#modules_addenda_the_mjs_extension // https://nodejs.org/docs/v12.7.0/api/esm.html#esm_code_import_code_expressions // https://github.com/qunitjs/qunit/issues/1465 try { require(filePath); } catch (e) { if ( (e.code === 'ERR_REQUIRE_ESM' || (e instanceof SyntaxError && e.message === 'Cannot use import statement outside a module')) && (!nodeVint || nodeVint >= 72) ) { // filePath is an absolute file path here (per path.resolve above). // On Windows, Node.js enforces that absolute paths via ESM use valid URLs, // e.g. file-protocol) https://github.com/qunitjs/qunit/issues/1667 await import(url.pathToFileURL(filePath)); // eslint-disable-line node/no-unsupported-features/es-syntax } else { throw e; } } } catch (e) { const error = new Error(`Failed to load file ${files[i]}\n${e.name}: ${e.message}`); error.stack = e.stack; QUnit.onUncaughtException(error); } } // Handle the unhandled process.on('unhandledRejection', onUnhandledRejection); process.on('uncaughtException', onUncaughtException); running = true; process.on('exit', onExit); QUnit.on('error', function (_error) { // Set exitCode directly, to make sure it is set to fail even if "runEnd" will never be // reached, or if "runEnd" was already fired in the past and the process crashed later. process.exitCode = 1; }); QUnit.on('runEnd', function setExitCode (data) { running = false; if (data.testCounts.failed) { process.exitCode = 1; } else { process.exitCode = 0; } }); QUnit.start(); } run.restart = function restart (args, options) { clearTimeout(restartDebounceTimer); restartDebounceTimer = setTimeout(() => { changedPendingPurge.forEach(file => delete require.cache[path.resolve(file)]); changedPendingPurge.length = 0; if (QUnit.config.queue.length) { console.log('Finishing current test and restarting...'); } else { console.log('Restarting...'); } abort(() => run(args, options)); }, DEBOUNCE_RESTART_LENGTH); }; function abort (callback) { function clearQUnit () { process.off('unhandledRejection', onUnhandledRejection); process.off('uncaughtException', onUncaughtException); process.off('exit', onExit); running = false; delete global.QUnit; QUnit = null; if (callback) { callback(); } } if (QUnit.config.queue.length) { const nextTestIndex = QUnit.config.queue.findIndex(fn => fn.name === 'runTest'); QUnit.config.queue.splice(nextTestIndex); QUnit.on('runEnd', clearQUnit); } else { clearQUnit(); } } run.watch = function watch (args, options) { const watch = require('node-watch'); const baseDir = process.cwd(); QUnit = requireQUnit(); global.QUnit = QUnit; options.requires.forEach(requireFromCWD); // Include TypeScript when in use (automatically via require.extensions), // https://github.com/qunitjs/qunit/issues/1669. // // Include ".json" (part of require.extensions) for test suites that use a data files, // and for changes to package.json that may affect how a file is parsed (e.g. type=module). // // Include ".cjs" and ".mjs", which Node.js doesn't expose via require.extensions by default. // // eslint-disable-next-line node/no-deprecated-api const includeExts = Object.keys(require.extensions).concat(['.cjs', '.mjs']); const ignoreDirs = ['.git', 'node_modules']; const watcher = watch(baseDir, { persistent: true, recursive: true, // Bare minimum delay, we have another debounce in restart(). delay: DEBOUNCE_WATCH_LENGTH, filter: (fullpath, skip) => { if (/\/node_modules\//.test(fullpath) || ignoreDirs.includes(path.basename(fullpath)) ) { return skip; } return includeExts.includes(path.extname(fullpath)); } }, (event, fullpath) => { console.log(`File ${event}: ${path.relative(baseDir, fullpath)}`); changedPendingPurge.push(fullpath); run.restart(args, options); }); watcher.on('ready', () => { run(args, options); }); function stop () { console.log('Stopping QUnit...'); watcher.close(); abort(() => { process.exit(); }); } process.on('SIGTERM', stop); process.on('SIGINT', stop); }; module.exports = run;