From 8d6ddc83c73f0d5fe191ca2a72eea522634e2837 Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Wed, 4 Sep 2024 19:34:50 -0700 Subject: [PATCH] Core: Add automatic labels in test.each() for primitive values in arrays Fixes https://github.com/qunitjs/qunit/issues/1733. --- docs/api/QUnit/test.each.md | 13 ++++++ src/core/test.js | 46 ++++++++++++++++++++- test/cli/fixtures/each-array-labels.js | 44 ++++++++++++++++++++ test/cli/fixtures/each-array-labels.tap.txt | 36 ++++++++++++++++ test/cli/fixtures/test-if.tap.txt | 8 ++-- test/main/promise.js | 6 +-- 6 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 test/cli/fixtures/each-array-labels.js create mode 100644 test/cli/fixtures/each-array-labels.tap.txt diff --git a/docs/api/QUnit/test.each.md b/docs/api/QUnit/test.each.md index e342cc057..3936c6a99 100644 --- a/docs/api/QUnit/test.each.md +++ b/docs/api/QUnit/test.each.md @@ -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 @@ -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 diff --git a/src/core/test.js b/src/core/test.js index 3e8d7e851..d025680f5 100644 --- a/src/core/test.js +++ b/src/core/test.js @@ -929,10 +929,54 @@ 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 +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) { diff --git a/test/cli/fixtures/each-array-labels.js b/test/cli/fixtures/each-array-labels.js new file mode 100644 index 000000000..885e1a746 --- /dev/null +++ b/test/cli/fixtures/each-array-labels.js @@ -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 strings', [ + 'foo', + '', + ' ', + 'x'.repeat(40), + '$', + 'http://example.org', + '999: example', + '\b', + '\n', + 'y'.repeat(100) +], function (assert, _data) { + assert.true(true); +}); + +QUnit.test.each('array of mixed', [ + undefined, + null, + false, + true, + 0, + 1, + -10, + 10 / 3, + 10e42, + Infinity, + NaN, + [], + {}, + 'some' +], 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); +}); diff --git a/test/cli/fixtures/each-array-labels.tap.txt b/test/cli/fixtures/each-array-labels.tap.txt new file mode 100644 index 000000000..a87e45d4e --- /dev/null +++ b/test/cli/fixtures/each-array-labels.tap.txt @@ -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 strings [foo] +ok 4 array of strings [] +ok 5 array of strings [ ] +ok 6 array of strings [xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] +ok 7 array of strings [$] +ok 8 array of strings [http://example.org] +ok 9 array of strings [6: 999: example] +ok 10 array of strings [7] +ok 11 array of strings [8] +ok 12 array of strings [9: yyyyyyyyyyyyyyyyyyyyyyyyyyyyy…] +ok 13 array of mixed [0: undefined] +ok 14 array of mixed [1: null] +ok 15 array of mixed [2: false] +ok 16 array of mixed [3: true] +ok 17 array of mixed [4: 0] +ok 18 array of mixed [5: 1] +ok 19 array of mixed [6: -10] +ok 20 array of mixed [7: 3.3333333333333335] +ok 21 array of mixed [8: 1e+43] +ok 22 array of mixed [9: Infinity] +ok 23 array of mixed [10: NaN] +ok 24 array of mixed [11] +ok 25 array of mixed [12] +ok 26 array of mixed [some] +ok 27 keyed objects [caseFoo] +ok 28 keyed objects [caseBar] +1..28 +# pass 28 +# skip 0 +# todo 0 +# fail 0 diff --git a/test/cli/fixtures/test-if.tap.txt b/test/cli/fixtures/test-if.tap.txt index fc4ead929..f8e6bedf1 100644 --- a/test/cli/fixtures/test-if.tap.txt +++ b/test/cli/fixtures/test-if.tap.txt @@ -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 diff --git a/test/main/promise.js b/test/main/promise.js index 1ad7e0fa4..0c16ef996 100644 --- a/test/main/promise.js +++ b/test/main/promise.js @@ -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' ); };