Skip to content

Commit

Permalink
Core: Add automatic labels in test.each() for primitive values in arrays
Browse files Browse the repository at this point in the history
Fixes #1733.
  • Loading branch information
Krinkle committed Sep 5, 2024
1 parent 0231379 commit 450ce0e
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 8 deletions.
13 changes: 13 additions & 0 deletions docs/api/QUnit/test.each.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ Each test case is passed one value of your dataset.

The [`only`](./test.only.md), [`todo`](./test.todo.md), [`skip`](./test.skip.md), and [`if`](./test.if.md) variants are also available, as `QUnit.test.only.each`, `QUnit.test.todo.each`, `QUnit.test.skip.each`, and `QUnit.test.if.each` respectively.

## Changelog

| UNRELEASED | Add [automatic labels](https://github.com/qunitjs/qunit/issues/1733) for primitive values in arrays.
| [QUnit 2.16.0](https://github.com/qunitjs/qunit/releases/tag/2.16.0) | Introduce `QUnit.test.each()`.

## Examples

### Basic data provider
Expand All @@ -50,6 +55,14 @@ function isEven (x) {
QUnit.test.each('isEven()', [2, 4, 6], (assert, data) => {
assert.true(isEven(data), `${data} is even`);
});

QUnit.test.each('truthy', ['a', 42, true, Infinity], (assert, data) => {
assert.true(!!data);
});

QUnit.test.each('falsy', [false, null], (assert, data) => {
assert.false(!!data);
});
```

### Array data provider
Expand Down
48 changes: 47 additions & 1 deletion src/core/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -929,10 +929,56 @@ function makeEachTestName (testName, argument) {
return `${testName} [${argument}]`;
}

// Characters to avoid in test names especially CLI/AP output:
// * x00-1F: e.g. NULL, backspace (\b), line breaks (\r\n), ESC.
// * x74: DEL.
// * xA0: non-breaking space.
//
// See https://en.wikipedia.org/wiki/ASCII#Character_order
//
// eslint-disable-next-line no-control-regex
const rNonObviousStr = /[\x00-\x1F\x7F\xA0]/;
function runEach (data, eachFn) {
if (Array.isArray(data)) {
for (let i = 0; i < data.length; i++) {
eachFn(data[i], i);
const value = data[i];

// Create automatic labels for primitive data in arrays passed to test.each().
// We want to avoid the default "example [0], example [1]" where possible since
// these are not self-explanatory in results, and are also tedious to locate
// the source of since the numerical key of an array isn't literally in the
// code (you have to count).
//
// Design requirements:
// * Unique. Each label must be unique and correspond 1:1 with a data value.
// This way each test name will hash to a unique testId with Rerun link,
// without having to rely on Test class enforcing uniqueness with invisible
// space hack.
// * Unambigious. While technical uniqueness is a hard requirement above,
// we also want the labels to be obvious and unambiguous to humans.
// For example, abbrebating "foobar" and "foobaz" to "f" and "fo" is
// technically unique, but ambigious to humans which one is which.
// * Short and readable. Where possible we omit the array index numbers
// so that in most cases, the value is simply shown as-is.
// We prefer "example [foo], example [bar]"
// over "example [0: foo], example [2: bar]".
// This also has the benefit of being stable and robust against e.g.
// re-ordering data or adding new items during development, without
// invalidating a previous filter or rerun link immediately.
const valueType = typeof value;
let testKey = i;
if (valueType === 'string' && value.length <= 40 && !rNonObviousStr.test(value) && !/\s*\d+: /.test(value)) {
testKey = value;
} else if (valueType === 'string' || valueType === 'number' || valueType === 'boolean' || valueType === 'undefined' || value === null) {
const valueForName = String(value);
if (!rNonObviousStr.test(valueForName)) {
testKey = i + ': ' + (valueForName.length <= 30
? valueForName
: valueForName.slice(0, 29) + '…'
);
}
}
eachFn(value, testKey);
}
} else if (typeof data === 'object' && data !== null) {
for (let key in data) {
Expand Down
43 changes: 43 additions & 0 deletions test/cli/fixtures/each-array-labels.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Automatic labels for test.each() array data where possible
// https://github.com/qunitjs/qunit/issues/1733

QUnit.test.each('array of arrays', [[1, 2, 3], [1, 1, 2]], function (assert, _data) {
assert.true(true);
});

QUnit.test.each('array of simple strings', [
'foo',
'x'.repeat(40),
'$',
'http://example.org',
' ',
''
], function (assert, _data) {
assert.true(true);
});

QUnit.test.each('array of mixed', [
undefined,
null,
false,
true,
0,
1,
-10,
10 / 3,
10e42,
Infinity,
NaN,
[],
{},
'999: example',
'\b',
'\n',
'y'.repeat(100)
], function (assert, _value) {
assert.true(true);
});

QUnit.test.each('keyed objects', { caseFoo: [1, 2, 3], caseBar: [1, 1, 2] }, function (assert, _data) {
assert.true(true);
});
35 changes: 35 additions & 0 deletions test/cli/fixtures/each-array-labels.tap.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# command: ["qunit", "each-array-labels.js"]

TAP version 13
ok 1 array of arrays [0]
ok 2 array of arrays [1]
ok 3 array of simple strings [foo]
ok 4 array of simple strings [xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx]
ok 5 array of simple strings [$]
ok 6 array of simple strings [http://example.org]
ok 7 array of simple strings [ ]
ok 8 array of simple strings []
ok 9 array of mixed [0: undefined]
ok 10 array of mixed [1: null]
ok 11 array of mixed [2: false]
ok 12 array of mixed [3: true]
ok 13 array of mixed [4: 0]
ok 14 array of mixed [5: 1]
ok 15 array of mixed [6: -10]
ok 16 array of mixed [7: 3.3333333333333335]
ok 17 array of mixed [8: 1e+43]
ok 18 array of mixed [9: Infinity]
ok 19 array of mixed [10: NaN]
ok 20 array of mixed [11]
ok 21 array of mixed [12]
ok 22 array of mixed [13: 999: example]
ok 23 array of mixed [14]
ok 24 array of mixed [15]
ok 25 array of mixed [16: yyyyyyyyyyyyyyyyyyyyyyyyyyyyy…]
ok 26 keyed objects [caseFoo]
ok 27 keyed objects [caseBar]
1..27
# pass 27
# skip 0
# todo 0
# fail 0
8 changes: 4 additions & 4 deletions test/cli/fixtures/test-if.tap.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ TAP version 13
ok 1 # SKIP skip me
ok 2 keep me
ok 3 regular
ok 4 # SKIP skip dataset [0]
ok 5 # SKIP skip dataset [1]
ok 6 keep dataset [0]
ok 7 keep dataset [1]
ok 4 # SKIP skip dataset [a]
ok 5 # SKIP skip dataset [b]
ok 6 keep dataset [a]
ok 7 keep dataset [b]
ok 8 # SKIP skip group > skipper
ok 9 keep group > keeper
1..9
Expand Down
6 changes: 3 additions & 3 deletions test/main/promise.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,21 +247,21 @@ QUnit.module('Support for Promise', function () {
}
});

QUnit.test.each('fulfilled Promise', [1], function (assert, _data) {
QUnit.test.each('fulfilled Promise', ['x'], function (assert, _data) {
assert.expect(1);

// Adds 1 assertion
return createMockPromise(assert);
});

QUnit.test.each('rejected Promise with Error', [1], function (assert, _data) {
QUnit.test.each('rejected Promise with Error', ['x'], function (assert, _data) {
assert.expect(2);

this.pushFailure = assert.test.pushFailure;
assert.test.pushFailure = function (message) {
assert.strictEqual(
message,
'Promise rejected during "rejected Promise with Error [0]": this is an error'
'Promise rejected during "rejected Promise with Error [x]": this is an error'
);
};

Expand Down

0 comments on commit 450ce0e

Please sign in to comment.