"use strict"; const path = require( "path" ); const requireFromCWD = require( "./require-from-cwd" ); const requireQUnit = require( "./require-qunit" ); const utils = require( "./utils" ); const IGNORED_GLOBS = [ "**/node_modules/**" ]; const RESTART_DEBOUNCE_LENGTH = 200; let QUnit; 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; } const seed = options.seed; if ( seed ) { if ( seed === true ) { QUnit.config.seed = Math.random().toString( 36 ).slice( 2 ); } else { QUnit.config.seed = 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 ); options.reporter.init( QUnit ); for ( let i = 0; i < files.length; i++ ) { const filePath = path.resolve( process.cwd(), files[ i ] ); delete require.cache[ filePath ]; try { require( filePath ); } catch ( e ) { // eslint-disable-next-line no-loop-func QUnit.module( files[ i ], function() { const loadFailureMessage = `Failed to load the test file with error:\n${e.stack}`; QUnit.test( loadFailureMessage, function( assert ) { assert.ok( false, "should be able to load file" ); } ); } ); } } let running = true; // Listen for unhandled rejections, and call QUnit.onUnhandledRejection process.on( "unhandledRejection", function( reason ) { QUnit.onUnhandledRejection( reason ); } ); process.on( "uncaughtException", function( error ) { QUnit.onError( error ); } ); process.on( "exit", function() { if ( running ) { console.error( "Error: Process exited before tests finished running" ); const currentTest = QUnit.config.current; if ( currentTest && currentTest.semaphore ) { 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." ); } } } ); QUnit.start(); QUnit.on( "runEnd", function setExitCode( data ) { running = false; if ( data.testCounts.failed ) { process.exitCode = 1; } else { process.exitCode = 0; } } ); } run.restart = function( args ) { clearTimeout( this._restartDebounceTimer ); this._restartDebounceTimer = setTimeout( () => { const watchedFiles = utils.findFiles( process.cwd(), { match: [ "**/*.js" ], ignore: IGNORED_GLOBS } ); watchedFiles.forEach( file => delete require.cache[ path.resolve( file ) ] ); if ( QUnit.config.queue.length ) { console.log( "Finishing current test and restarting..." ); } else { console.log( "Restarting..." ); } run.abort( () => run.apply( null, args ) ); }, RESTART_DEBOUNCE_LENGTH ); }; run.abort = function( callback ) { function clearQUnit() { 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() { const watch = require( "node-watch" ); const args = Array.prototype.slice.call( arguments ); const baseDir = process.cwd(); const watcher = watch( baseDir, { persistent: true, recursive: true, delay: 0, filter: ( fullpath ) => { return !/\/node_modules\//.test( fullpath ) && /\.js$/.test( fullpath ); } }, ( event, fullpath ) => { console.log( `File ${event}: ${path.relative( baseDir, fullpath )}` ); run.restart( args ); } ); watcher.on( "ready", () => { run.apply( null, args ); } ); function stop() { console.log( "Stopping QUnit..." ); watcher.close(); run.abort( () => { process.exit(); } ); } process.on( "SIGTERM", stop ); process.on( "SIGINT", stop ); }; module.exports = run;