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 @@ + + + + + QUnit.test.each Suite + + + + + +
+
test markup
+ + diff --git a/test/main/each.js b/test/main/each.js new file mode 100644 index 000000000..c581b04e3 --- /dev/null +++ b/test/main/each.js @@ -0,0 +1,32 @@ +QUnit.module( "test.each", function() { + QUnit.test.each( "test.each", [ [ 1, 2, 3 ], [ 1, 1, 2 ] ], function( assert, data ) { + assert.strictEqual( data[ 0 ] + data[ 1 ], data[ 2 ] ); + } ); + QUnit.test.each( "test.each returning a Promise", [ [ 1, 2, 3 ], [ 1, 1, 2 ] ], function( assert, data ) { + function sum( a, b ) { + return new Promise( function( resolve ) { + resolve( a + b ); + } ); + } + return sum( data[ 0 ], data[ 1 ] ).then( function( result ) { assert.strictEqual( result, data[ 2 ] ); } ); + } ); + QUnit.test.each( "test.each 1D", [ 1, [], "some" ], function( assert, value ) { + assert.true( Boolean( value ) ); + } ); + QUnit.test.each( "test.each fails with non-array input", [ "something", 1, undefined, null, {} ], function( assert, value ) { + assert.throws( function() { + QUnit.test.each( "test.each 1D", value, function() { } ); + } ); + } ); +} ); +QUnit.module( "test.skip.each", function() { + QUnit.test( "do run", function( assert ) { assert.true( true ); } ); + QUnit.test.skip.each( "test.skip.each", [ [ 1, 2, 3 ], [ 1, 1, 2 ] ], function( assert ) { + assert.true( false ); + } ); +} ); +QUnit.module( "test.todo.each", function() { + QUnit.test.todo.each( "test.todo.each", [ [ 1, 2, 3 ], [ 1, 1, 2 ] ], function( assert ) { + assert.true( false ); + } ); +} ); diff --git a/test/main/only-each.js b/test/main/only-each.js new file mode 100644 index 000000000..63460a597 --- /dev/null +++ b/test/main/only-each.js @@ -0,0 +1,9 @@ +QUnit.module.only( "test.each.only", function() { + QUnit.test( "don't run", function( assert ) { assert.true( false ); } ); + QUnit.test.only.each( "test.each.only", [ [ 1, 2, 3 ], [ 1, 1, 2 ] ], function( assert, data ) { + assert.strictEqual( data[ 0 ] + data[ 1 ], data[ 2 ] ); + } ); + QUnit.test.only.each( "test.each.only 1D", [ 1, [], "some" ], function( assert, value ) { + assert.true( Boolean( value ) ); + } ); +} ); diff --git a/test/only-each.html b/test/only-each.html new file mode 100644 index 000000000..9ae78f8e9 --- /dev/null +++ b/test/only-each.html @@ -0,0 +1,14 @@ + + + + + QUnit.test.each.only Suite + + + + + +
+
test markup
+ +