├── .npmrc ├── .gitattributes ├── .gitignore ├── test ├── fixtures │ ├── fixture-require1.js │ ├── fixture-require2.js │ ├── fixture-fail.js │ ├── fixture-pass.js │ ├── fixture-throws.js │ ├── fixture-async.js │ └── fixture-throws-uncaught.js └── test.js ├── .github ├── security.md └── workflows │ └── main.yml ├── gulpfile.js ├── .editorconfig ├── license ├── package.json ├── index.js └── readme.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /test/fixtures/fixture-require1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | -------------------------------------------------------------------------------- /test/fixtures/fixture-require2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | -------------------------------------------------------------------------------- /test/fixtures/fixture-fail.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | 3 | it('should fail', () => { 4 | assert(false); 5 | }); 6 | -------------------------------------------------------------------------------- /test/fixtures/fixture-pass.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | 3 | it('should pass', () => { 4 | assert(true); 5 | }); 6 | -------------------------------------------------------------------------------- /test/fixtures/fixture-throws.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | 3 | it('contains syntax errors', () => { 4 | assert false; 5 | }); 6 | -------------------------------------------------------------------------------- /test/fixtures/fixture-async.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | 3 | it('should fail after timeout', done => { 4 | setTimeout(() => { 5 | assert(false); 6 | }, 10); 7 | }); 8 | -------------------------------------------------------------------------------- /.github/security.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import mocha from './index.js'; 3 | 4 | export default function main() { 5 | return gulp.src('test/fixtures/fixture-pass.js', {read: false}) 6 | .pipe(mocha()); 7 | } 8 | 9 | -------------------------------------------------------------------------------- /test/fixtures/fixture-throws-uncaught.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | 3 | it('throws after timeout', () => { 4 | setTimeout(() => { 5 | throw new Error('Exception in delayed function'); 6 | }, 10); 7 | }); 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 20 14 | - 18 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-mocha", 3 | "version": "10.0.1", 4 | "description": "Run Mocha tests", 5 | "license": "MIT", 6 | "repository": "sindresorhus/gulp-mocha", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": "./index.js", 15 | "sideEffects": false, 16 | "engines": { 17 | "node": ">=18" 18 | }, 19 | "scripts": { 20 | "test": "xo && ava" 21 | }, 22 | "files": [ 23 | "index.js" 24 | ], 25 | "keywords": [ 26 | "gulpplugin", 27 | "mocha", 28 | "test", 29 | "testing", 30 | "unit", 31 | "framework", 32 | "runner", 33 | "tdd", 34 | "bdd", 35 | "qunit", 36 | "spec", 37 | "tap" 38 | ], 39 | "dependencies": { 40 | "dargs": "^8.1.0", 41 | "execa": "^8.0.1", 42 | "gulp-plugin-extras": "^0.3.0", 43 | "mocha": "^10.2.0", 44 | "supports-color": "^9.4.0" 45 | }, 46 | "devDependencies": { 47 | "ava": "^5.3.1", 48 | "gulp": "^4.0.2", 49 | "p-event": "^6.0.0", 50 | "vinyl": "^3.0.0", 51 | "xo": "^0.56.0" 52 | }, 53 | "peerDependencies": { 54 | "gulp": ">=4" 55 | }, 56 | "peerDependenciesMeta": { 57 | "gulp": { 58 | "optional": true 59 | } 60 | }, 61 | "xo": { 62 | "ignores": [ 63 | "test/fixtures" 64 | ], 65 | "rules": { 66 | "ava/no-ignored-test-files": "off" 67 | } 68 | }, 69 | "ava": { 70 | "files": [ 71 | "test/test.js" 72 | ] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import {fileURLToPath} from 'node:url'; 3 | import path from 'node:path'; 4 | import test from 'ava'; 5 | import Vinyl from 'vinyl'; 6 | import {pEvent} from 'p-event'; 7 | import mocha from '../index.js'; 8 | 9 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 10 | 11 | function fixture(name) { 12 | const filename = path.join(__dirname, 'fixtures', name); 13 | 14 | return new Vinyl({ 15 | path: filename, 16 | contents: fs.existsSync(filename) ? fs.readFileSync(filename) : null, 17 | }); 18 | } 19 | 20 | test('run unit test and pass', async t => { 21 | const stream = mocha({suppress: true}); 22 | const result = pEvent(stream, '_result'); 23 | stream.end(fixture('fixture-pass.js')); 24 | const {stdout} = await result; 25 | t.regex(stdout, /1 passing/); 26 | }); 27 | 28 | test('run unit test and fail', async t => { 29 | const stream = mocha({suppress: true}); 30 | const error = pEvent(stream, 'error'); 31 | stream.end(fixture('fixture-fail.js')); 32 | const {message} = await error; 33 | t.regex(message, /There were test failures/); 34 | }); 35 | 36 | test('pass async AssertionError to mocha', async t => { 37 | const stream = mocha({suppress: true}); 38 | const event = pEvent(stream, 'error'); 39 | stream.end(fixture('fixture-async.js')); 40 | const error = await event; 41 | t.regex(error.message, /There were test failures/); 42 | }); 43 | 44 | test('require two files', async t => { 45 | const stream = mocha({ 46 | suppress: true, 47 | require: [ 48 | 'test/fixtures/fixture-require1.js', 49 | 'test/fixtures/fixture-require2.js', 50 | ], 51 | }); 52 | const result = pEvent(stream, '_result'); 53 | stream.end(fixture('fixture-pass.js')); 54 | const {stdout} = await result; 55 | t.regex(stdout, /1 passing/); 56 | }); 57 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import {fileURLToPath} from 'node:url'; 3 | import path from 'node:path'; 4 | import dargs from 'dargs'; 5 | import {execa} from 'execa'; 6 | import supportsColor from 'supports-color'; 7 | import {gulpPlugin} from 'gulp-plugin-extras'; 8 | 9 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 10 | 11 | // Mocha options that can be specified multiple times 12 | const MULTIPLE_OPTIONS = new Set([ 13 | 'require', 14 | ]); 15 | 16 | function convertObjectToList(object) { 17 | return Object.entries(object) 18 | .map(([key, value]) => `${key}=${value}`) 19 | .join(','); 20 | } 21 | 22 | export default function gulpMocha(options) { 23 | options = { 24 | colors: Boolean(supportsColor.stdout), 25 | suppress: false, 26 | ...options, 27 | }; 28 | 29 | for (const [key, value] of Object.entries(options)) { 30 | if (Array.isArray(value)) { 31 | if (!MULTIPLE_OPTIONS.has(key)) { 32 | // Convert arrays into comma separated lists 33 | options[key] = value.join(','); 34 | } 35 | } else if (typeof value === 'object') { 36 | // Convert an object into comma separated list 37 | options[key] = convertObjectToList(value); 38 | } 39 | } 40 | 41 | const arguments_ = dargs(options, { 42 | excludes: ['suppress'], 43 | ignoreFalse: true, 44 | }); 45 | 46 | const files = []; 47 | 48 | return gulpPlugin('gulp-mocha', file => { 49 | files.push(file.path); 50 | }, { 51 | supportsAnyType: true, 52 | async * onFinish(stream) { // eslint-disable-line require-yield 53 | const subprocess = execa('mocha', [...files, ...arguments_], { 54 | localDir: __dirname, 55 | preferLocal: true, 56 | }); 57 | 58 | if (!options.suppress) { 59 | subprocess.stdout.pipe(process.stdout); 60 | subprocess.stderr.pipe(process.stderr); 61 | } 62 | 63 | try { 64 | const result = await subprocess; 65 | stream.emit('_result', result); 66 | } catch (error) { 67 | if (error.exitCode > 0) { 68 | const error = new Error('There were test failures'); 69 | error.isPresentable = true; 70 | throw error; 71 | } 72 | 73 | throw error; 74 | } 75 | }, 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # gulp-mocha 2 | 3 | > Run [Mocha](https://github.com/mochajs/mocha) tests 4 | 5 | *Keep in mind that this is just a thin wrapper around Mocha and your issue is most likely with Mocha.* 6 | 7 | ## Install 8 | 9 | ```sh 10 | npm install --save-dev gulp-mocha 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```js 16 | import gulp from 'gulp'; 17 | import mocha from 'gulp-mocha'; 18 | 19 | export default () => ( 20 | gulp.src('test.js', {read: false}) 21 | // `gulp-mocha` needs file paths so you cannot have any plugins before it. 22 | .pipe(mocha({reporter: 'nyan'})) 23 | ); 24 | ``` 25 | 26 | ## API 27 | 28 | ### mocha(options?) 29 | 30 | #### options 31 | 32 | Type: `object` 33 | 34 | Options are passed directly to the `mocha` binary, so you can use any its [command-line options](http://mochajs.org/#usage) in a camelCased form. Arrays and key/value objects are correctly converted to the comma separated list format Mocha expects. Listed below are some of the more commonly used options: 35 | 36 | ##### ui 37 | 38 | Type: `string`\ 39 | Default: `'bdd'`\ 40 | Values: `'bdd' | 'tdd' | 'qunit' | 'exports'` 41 | 42 | The interface to use. 43 | 44 | ##### reporter 45 | 46 | Type: `string`\ 47 | Default: `spec`\ 48 | Values: [Reporters](https://github.com/mochajs/mocha/tree/master/lib/reporters) 49 | 50 | The reporter that will be used. 51 | 52 | This option can also be used to utilize third-party reporters. For example, if you `npm install mocha-lcov-reporter` you can then do use `mocha-lcov-reporter` as value. 53 | 54 | ##### reporterOptions 55 | 56 | Type: `object`\ 57 | Example: `{reportFilename: 'index.html'}` 58 | 59 | Reporter specific options. 60 | 61 | ##### globals 62 | 63 | Type: `string[]` 64 | 65 | List of accepted global variable names, example `['YUI']`. Accepts wildcards to match multiple global variables, e.g. `['gulp*']` or even `['*']`. See [Mocha globals option](http://mochajs.org/#globals-option). 66 | 67 | ##### timeout 68 | 69 | Type: `number`\ 70 | Default: `2000` 71 | 72 | Test-case timeout in milliseconds. 73 | 74 | ##### bail 75 | 76 | Type: `boolean`\ 77 | Default: `false` 78 | 79 | Bail on the first test failure. 80 | 81 | ##### checkLeaks 82 | 83 | Type: `boolean`\ 84 | Default: `false` 85 | 86 | Check for global variable leaks. 87 | 88 | ##### grep 89 | 90 | Type: `string` 91 | 92 | Only run tests matching the given pattern which is internally compiled to a RegExp. 93 | 94 | ##### require 95 | 96 | Type: `string[]` 97 | 98 | Require custom modules before tests are run. 99 | 100 | ##### compilers 101 | 102 | Type: `string`\ 103 | Example: `js:babel-core/register` 104 | 105 | Specify a compiler. 106 | 107 | ## FAQ 108 | 109 | ### Test suite not exiting 110 | 111 | If your test suite is not exiting it might be because you still have a lingering callback, most often caused by an open database connection. You should close this connection or do the following: 112 | 113 | ```js 114 | export default () => ( 115 | gulp.src('test.js') 116 | .pipe(mocha()) 117 | .once('error', err => { 118 | console.error(err); 119 | process.exit(1); 120 | }) 121 | .once('end', () => { 122 | process.exit(); 123 | }) 124 | ); 125 | ``` 126 | 127 | Or you might just need to pass the `exit` option: 128 | 129 | ```js 130 | export const test = () => ( 131 | gulp.src(['test/**/*.js'], {read: false}) 132 | .pipe(mocha({reporter: 'list', exit: true})) 133 | .on('error', console.error) 134 | ); 135 | ``` 136 | --------------------------------------------------------------------------------