diff --git a/Gruntfile.js b/Gruntfile.js index f04f16a42..4877c47a6 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -102,6 +102,8 @@ module.exports = function( grunt ) { "test/string-filter.html", "test/module-skip.html", "test/module-todo.html", + "test/each.html", + "test/only-each.html", // ensure this is last - it has the potential to drool // and omit subsequent tests during coverage runs diff --git a/docs/QUnit/test.each.md b/docs/QUnit/test.each.md new file mode 100644 index 000000000..e63ae9df1 --- /dev/null +++ b/docs/QUnit/test.each.md @@ -0,0 +1,73 @@ +--- +layout: default +title: QUnit.test.each() +excerpt: Add a parameterized test to run. +categories: + - main +version_added: "unreleased" +--- + +`QUnit.test.each( name, data, callback )` + +Add a parameterized test to run. + +| parameter | description | +|-----------|-------------| +| `name` (string) | Title of unit being tested | +| `data` (Array) | Array of arrays of parameters to be passed as input to each test. This can also be specified as a 1D array of primitives | +| `callback` (function) | Function to close over assertions | + +#### Callback parameters: `callback( assert, args )`: + +| parameter | description | +|-----------|-------------| +| `assert` (object) | A new instance object with the [assertion methods](../assert/index.md) | +| `args` (any) | All input parameters. The original array is passed. Array destructuring can be used to unpack input values | + +### Description + +Add a parameterized test to run using `QUnit.test.each()`. `QUnit.test.each()` generates multiple calls to `QUnit.test()` so `then`-able behavior is maintained. + + +The `assert` argument to the callback contains all of QUnit's [assertion methods](../assert/index.md). Use this argument to call your test assertions. +`QUnit.test.only.each`, `QUnit.test.skip.each` and `QUnit.test.todo.each` are also available. + +See also: +* [`QUnit.test.only()`](./test.only.md) +* [`QUnit.test.skip()`](./test.skip.md) +* [`QUnit.test.todo()`](./test.todo.md) + + +### Examples + +A practical example, using the assert argument and no globals. + +```js +function square( x ) { + return x * x; +} + +function isEven( x ) { + return x % 2 === 0; +} + +function isAsyncEven( x ) { + return new Promise( resolve => { + return resolve( isEven( x ) ); + } ); +} + +QUnit.test.each( "square()", [ [ 2, 4 ], [ 3, 9 ] ], ( assert, [ value, expected ] ) => { + assert.equal( square( value ), expected, `square(${value})` ); +}); + +QUnit.test.each( "isEven()", [ 2, 4 ], ( assert, value ) => { + assert.true( isEven( value ), `isEven(${value})` ); +}); + +QUnit.test.each( "isAsyncEven()", [ 2, 4 ], ( assert, value ) => { + return isAsyncEven( value ).then( ( value ) => { + assert.true( isAsyncEven( value ), `isAsyncEven(${value})` ); + } ); +}); +``` diff --git a/src/test.js b/src/test.js index af5de3840..b2352b5a9 100644 --- a/src/test.js +++ b/src/test.js @@ -176,7 +176,7 @@ Test.prototype = { } function runTest( test ) { - const promise = test.callback.call( test.testEnvironment, test.assert ); + const promise = test.callback.call( test.testEnvironment, test.assert, test.params ); test.resolvePromise( promise ); // If the test has a "lock" on it, but the timeout is 0, then we push a @@ -674,66 +674,122 @@ function checkPollution() { } } +function addTestWithData( data ) { + if ( focused || config.currentModule.ignored ) { + return; + } + + const newTest = new Test( data ); + + newTest.queue(); +} + let focused = false; // indicates that the "only" filter was used // Will be exposed as QUnit.test export function test( testName, callback ) { + addTestWithData( { + testName: testName, + callback: callback + } ); +} + +function todo( testName, data, callback ) { if ( focused || config.currentModule.ignored ) { return; } const newTest = new Test( { - testName: testName, - callback: callback + testName, + callback, + todo: true, + params: data } ); newTest.queue(); } -extend( test, { - todo: function todo( testName, callback ) { - if ( focused || config.currentModule.ignored ) { - return; - } +function skip( testName ) { + if ( focused || config.currentModule.ignored ) { + return; + } - const newTest = new Test( { - testName, - callback, - todo: true - } ); + const test = new Test( { + testName: testName, + skip: true + } ); - newTest.queue(); - }, - skip: function skip( testName ) { - if ( focused || config.currentModule.ignored ) { - return; - } + test.queue(); +} +function only( testName, data, callback ) { + if ( config.currentModule.ignored ) { + return; + } + if ( !focused ) { + config.queue.length = 0; + focused = true; + } - const test = new Test( { - testName: testName, - skip: true - } ); + const newTest = new Test( { + testName: testName, + callback: callback, + params: data + } ); - test.queue(); - }, - only: function only( testName, callback ) { - if ( config.currentModule.ignored ) { - return; - } - if ( !focused ) { - config.queue.length = 0; - focused = true; - } + newTest.queue(); +} - const newTest = new Test( { - testName: testName, - callback: callback - } ); +function makeEachTestName( testName, argument ) { + return `${testName} [${argument}]`; +} + +function runEach( data, eachFn ) { + if ( Array.isArray( data ) ) { + data.forEach( eachFn ); + } else { + throw new Error( + `test.each expects an array of arrays or an array of primitives as the expected input. +${typeof data} was found instead.` + ); + } +} - newTest.queue(); +extend( test, { + todo: function( testName, callback ) { + todo( testName, undefined, callback ); + }, + skip, + only: function( testName, callback ) { + only( testName, undefined, callback ); + }, + each: function( testName, data, callback ) { + runEach( data, ( datum, i ) => { + addTestWithData( { + testName: makeEachTestName( testName, i ), + callback: callback, + params: datum + } ); + } ); } } ); +test.todo.each = function( testName, data, callback ) { + runEach( data, ( datum, i ) => { + todo( makeEachTestName( testName, i ), datum, callback ); + } ); +}; +test.skip.each = function( testName, data ) { + runEach( data, ( _, i ) => { + skip( makeEachTestName( testName, i ) ); + } ); +}; + +test.only.each = function( testName, data, callback ) { + runEach( data, ( datum, i ) => { + only( makeEachTestName( testName, i ), datum, callback ); + } ); +}; + // Resets config.timeout with a new timeout duration. export function resetTestTimeout( timeoutDuration ) { clearTimeout( config.timeout ); diff --git a/test/each.html b/test/each.html new file mode 100644 index 000000000..ef0385cc9 --- /dev/null +++ b/test/each.html @@ -0,0 +1,14 @@ + + +
+ +