Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core: Add automatic labels in test.each() for primitive values in arrays #1799

Merged
merged 1 commit into from
Sep 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
44 changes: 44 additions & 0 deletions test/cli/fixtures/each-array-labels.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// 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',
Krinkle marked this conversation as resolved.
Show resolved Hide resolved
'simple string',
'\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);
});
36 changes: 36 additions & 0 deletions test/cli/fixtures/each-array-labels.tap.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# 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]
Krinkle marked this conversation as resolved.
Show resolved Hide resolved
ok 23 array of mixed [simple string]
ok 24 array of mixed [15]
ok 25 array of mixed [16]
ok 26 array of mixed [17: yyyyyyyyyyyyyyyyyyyyyyyyyyyyy…]
ok 27 keyed objects [caseFoo]
ok 28 keyed objects [caseBar]
1..28
# pass 28
# 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