From bd7e45c8ec4260971f198f5f1217f1ae07183a6e Mon Sep 17 00:00:00 2001 From: Gleb Bahmutov Date: Fri, 15 Oct 2021 09:26:38 -0400 Subject: [PATCH] feat: add make test title function support (#9) * add test title function spec * feat: implement test title function * add readme section --- README.md | 31 ++++++++++++++++++++ cypress/integration/each-k.js | 26 +++++++++-------- cypress/integration/title-function.js | 42 +++++++++++++++++++++++++++ src/index.d.ts | 7 +++-- src/index.js | 40 +++++++++++++++++++++---- 5 files changed, 126 insertions(+), 20 deletions(-) create mode 100644 cypress/integration/title-function.js diff --git a/README.md b/README.md index a6baba0..9d0517c 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,37 @@ it.each([10, 20, 30])('checking item %K', (x) => { ... }) // "checking item 3" ``` +### Title function + +You can form the test title yourself using a function. The function will get the item, the index, and all items and should return a string with the test title. + +```js +function makeTestTitle(s, k, strings) { + return `test ${k + 1} for "${s}"` +} +it.each(['first', 'second'])(makeTestTitle, () => ...) +// creates the tests +// 'test 1 for "first"' +// 'test 2 for "second"' +``` + +It is very useful for forming a test title based on a property of an object, like + +```js +it.each([ + { name: 'Joe', age: 30 }, + { name: 'Mary', age: 20 }, +])( + (person) => `tests person ${person.name}`, + (person) => { ... } +}) +// creates the tests +// "tests person Joe" +// "tests person Mary" +``` + +See [cypress/integration/title-function.js](./cypress/integration/ title-function.js) for more examples + ## Examples - Watch [Using cypress-each To Create Separate Tests](https://youtu.be/utPKRV_fL1E) diff --git a/cypress/integration/each-k.js b/cypress/integration/each-k.js index ff31017..be96c1e 100644 --- a/cypress/integration/each-k.js +++ b/cypress/integration/each-k.js @@ -3,18 +3,20 @@ import '../..' -it('has test title', function () { - expect(this, 'has test').to.have.property('test') - // @ts-ignore - expect(this.test.title).to.equal('has test title') -}) +describe('Using %k and %K placeholders', () => { + it('has test title', function () { + expect(this, 'has test').to.have.property('test') + // @ts-ignore + expect(this.test.title).to.equal('has test title') + }) -it.each([1, 2, 3])('has 0-based index %k', function (x) { - expect(x).to.be.oneOf([1, 2, 3]) - // needs test context to be passed correctly by it.each - // expect(this.test.title).to.equal('has 0-based index 0') -}) + it.each([1, 2, 3])('has 0-based index %k', function (x) { + expect(x).to.be.oneOf([1, 2, 3]) + // needs test context to be passed correctly by it.each + // expect(this.test.title).to.equal('has 0-based index 0') + }) -it.each([1, 2, 3])('has 1-based index %K', (x) => { - expect(x).to.be.oneOf([1, 2, 3]) + it.each([1, 2, 3])('has 1-based index %K', (x) => { + expect(x).to.be.oneOf([1, 2, 3]) + }) }) diff --git a/cypress/integration/title-function.js b/cypress/integration/title-function.js new file mode 100644 index 0000000..46e05c8 --- /dev/null +++ b/cypress/integration/title-function.js @@ -0,0 +1,42 @@ +// @ts-check +/// + +import '../../src' + +/** + * Forms the test title from the item and index + * @param {string} s Individual string + * @param {number} k Item index (0-based) + * @param {string[]} strings All strings + * @returns + */ +function makeTestTitle(s, k, strings) { + expect(s, 'first argument is the item').to.equal('foo') + expect(k, 'second argument is the index').to.equal(0) + expect(strings, 'all items').to.deep.equal(['foo']) + + return `test ${k + 1} for "${s}"` +} + +describe('Form test title using a function', () => { + it.each(['foo'])(makeTestTitle, function (s) { + expect(s, 'item value').to.equal('foo') + // @ts-ignore + expect(this.test.title, 'computed test title').to.equal('test 1 for "foo"') + }) + + it.each([ + { name: 'Joe', age: 30 }, + { name: 'Mary', age: 20 }, + ])( + (person) => `tests person ${person.name}`, + function (user) { + expect(user).to.have.keys('name', 'age') + // @ts-ignore + expect(this.test.title, 'computed test title').to.be.oneOf([ + 'tests person Joe', + 'tests person Mary', + ]) + }, + ) +}) diff --git a/src/index.d.ts b/src/index.d.ts index 6ced071..05fca64 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,6 +1,9 @@ // types for it.each and describe.each // any help improving them is welcome // https://github.com/bahmutov/cypress-each + +type TestTitleFn = (item: T, index: number, items: T[]) => string + declare namespace Mocha { type TestCallback = (this: Context, arg0: T, arg1: any, arg2: any) => void @@ -8,13 +11,13 @@ declare namespace Mocha { // definition for it.each each( values: T[], - ): (titlePattern: string, fn: TestCallback) => void + ): (titlePattern: string | TestTitleFn, fn: TestCallback) => void } interface SuiteFunction { // definition for describe.each each( values: T[], - ): (titlePattern: string, fn: TestCallback) => void + ): (titlePattern: string | TestTitleFn, fn: TestCallback) => void } } diff --git a/src/index.js b/src/index.js index cac9fd0..0024eb5 100644 --- a/src/index.js +++ b/src/index.js @@ -11,20 +11,41 @@ function formatTitle(pattern, ...values) { return format.apply(null, [pattern].concat(values.slice(0, count))) } +function makeTitle(titlePattern, value, k, values) { + if (typeof titlePattern === 'string') { + const testTitle = titlePattern.replace('%k', k).replace('%K', k + 1) + if (Array.isArray(value)) { + return formatTitle(testTitle, ...value) + } else { + return formatTitle(testTitle, value) + } + } else if (typeof titlePattern === 'function') { + return titlePattern(value, k, values) + } + + throw new Error('titlePattern must be a string or function') +} + if (!it.each) { it.each = function (values) { return function (titlePattern, testCallback) { values.forEach(function (value, k) { - const testTitle = titlePattern.replace('%k', k).replace('%K', k + 1) + // const testTitle = titlePattern.replace('%k', k).replace('%K', k + 1) + const title = makeTitle(titlePattern, value, k, values) + if (!title) { + throw new Error( + `Could not compute the test title ${k} for value ${value}`, + ) + } // define a test for each value if (Array.isArray(value)) { - const title = formatTitle(testTitle, ...value) + // const title = formatTitle(testTitle, ...value) it(title, function itArrayCallback() { return testCallback.apply(this, value) }) } else { - const title = formatTitle(testTitle, value) + // const title = formatTitle(testTitle, value) it(title, function itCallback() { return testCallback.call(this, value) }) @@ -39,13 +60,20 @@ if (!describe.each) { return function describeEach(titlePattern, testCallback) { // define a test for each value values.forEach((value, k) => { - const testTitle = titlePattern.replace('%k', k).replace('%K', k + 1) + // const testTitle = titlePattern.replace('%k', k).replace('%K', k + 1) + const title = makeTitle(titlePattern, value, k, values) + + if (!title) { + throw new Error( + `Could not compute the suite title ${k} for value ${value}`, + ) + } if (Array.isArray(value)) { - const title = formatTitle(testTitle, ...value) + // const title = formatTitle(testTitle, ...value) describe(title, testCallback.bind(null, ...value)) } else { - const title = formatTitle(testTitle, value) + // const title = formatTitle(testTitle, value) describe(title, testCallback.bind(null, value)) } })