diff --git a/CHANGELOG.md b/CHANGELOG.md index f7e8e3b07be7..1853cc3957ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - `[jest-circus]` Fixed the issue of beforeAll & afterAll hooks getting executed even if it is inside a skipped `describe` block [#10451](https://github.com/facebook/jest/issues/10451) - `[jest-circus]` Fix `testLocation` on Windows when using `test.each` ([#10871](https://github.com/facebook/jest/pull/10871)) - `[jest-console]` `console.dir` now respects the second argument correctly ([#10638](https://github.com/facebook/jest/pull/10638)) +- `[jest-each]` [**BREAKING**] Ignore excess words in headings ([#8766](https://github.com/facebook/jest/pull/8766)) - `[jest-environment-jsdom]` Use inner realm’s `ArrayBuffer` constructor ([#10885](https://github.com/facebook/jest/pull/10885)) - `[jest-globals]` [**BREAKING**] Disallow return values other than a `Promise` from hooks and tests ([#10512](https://github.com/facebook/jest/pull/10512)) - `[jest-globals]` [**BREAKING**] Disallow mixing a done callback and returning a `Promise` from hooks and tests ([#10512](https://github.com/facebook/jest/pull/10512)) diff --git a/packages/jest-each/src/__tests__/__snapshots__/template.test.ts.snap b/packages/jest-each/src/__tests__/__snapshots__/template.test.ts.snap index a7f2fabc0be5..534f55a84220 100644 --- a/packages/jest-each/src/__tests__/__snapshots__/template.test.ts.snap +++ b/packages/jest-each/src/__tests__/__snapshots__/template.test.ts.snap @@ -5,6 +5,42 @@ exports[`jest-each .describe throws an error when called with an empty string 1` " `; +exports[`jest-each .describe throws error when there are additional words in first column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a is the left | b | expected + \\"" +`; + +exports[`jest-each .describe throws error when there are additional words in last column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a | b | expected value + \\"" +`; + +exports[`jest-each .describe throws error when there are additional words in second column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a | b is the right | expected + \\"" +`; + exports[`jest-each .describe throws error when there are fewer arguments than headings over multiple rows 1`] = ` "Not enough arguments supplied for given headings: a | b | expected @@ -44,6 +80,42 @@ exports[`jest-each .describe.only throws an error when called with an empty stri " `; +exports[`jest-each .describe.only throws error when there are additional words in first column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a is the left | b | expected + \\"" +`; + +exports[`jest-each .describe.only throws error when there are additional words in last column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a | b | expected value + \\"" +`; + +exports[`jest-each .describe.only throws error when there are additional words in second column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a | b is the right | expected + \\"" +`; + exports[`jest-each .describe.only throws error when there are fewer arguments than headings over multiple rows 1`] = ` "Not enough arguments supplied for given headings: a | b | expected @@ -83,6 +155,42 @@ exports[`jest-each .fdescribe throws an error when called with an empty string 1 " `; +exports[`jest-each .fdescribe throws error when there are additional words in first column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a is the left | b | expected + \\"" +`; + +exports[`jest-each .fdescribe throws error when there are additional words in last column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a | b | expected value + \\"" +`; + +exports[`jest-each .fdescribe throws error when there are additional words in second column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a | b is the right | expected + \\"" +`; + exports[`jest-each .fdescribe throws error when there are fewer arguments than headings over multiple rows 1`] = ` "Not enough arguments supplied for given headings: a | b | expected @@ -122,6 +230,42 @@ exports[`jest-each .fit throws an error when called with an empty string 1`] = ` " `; +exports[`jest-each .fit throws error when there are additional words in first column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a is the left | b | expected + \\"" +`; + +exports[`jest-each .fit throws error when there are additional words in last column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a | b | expected value + \\"" +`; + +exports[`jest-each .fit throws error when there are additional words in second column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a | b is the right | expected + \\"" +`; + exports[`jest-each .fit throws error when there are fewer arguments than headings over multiple rows 1`] = ` "Not enough arguments supplied for given headings: a | b | expected @@ -161,6 +305,42 @@ exports[`jest-each .it throws an error when called with an empty string 1`] = ` " `; +exports[`jest-each .it throws error when there are additional words in first column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a is the left | b | expected + \\"" +`; + +exports[`jest-each .it throws error when there are additional words in last column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a | b | expected value + \\"" +`; + +exports[`jest-each .it throws error when there are additional words in second column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a | b is the right | expected + \\"" +`; + exports[`jest-each .it throws error when there are fewer arguments than headings over multiple rows 1`] = ` "Not enough arguments supplied for given headings: a | b | expected @@ -200,6 +380,42 @@ exports[`jest-each .it.only throws an error when called with an empty string 1`] " `; +exports[`jest-each .it.only throws error when there are additional words in first column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a is the left | b | expected + \\"" +`; + +exports[`jest-each .it.only throws error when there are additional words in last column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a | b | expected value + \\"" +`; + +exports[`jest-each .it.only throws error when there are additional words in second column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a | b is the right | expected + \\"" +`; + exports[`jest-each .it.only throws error when there are fewer arguments than headings over multiple rows 1`] = ` "Not enough arguments supplied for given headings: a | b | expected @@ -239,6 +455,42 @@ exports[`jest-each .test throws an error when called with an empty string 1`] = " `; +exports[`jest-each .test throws error when there are additional words in first column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a is the left | b | expected + \\"" +`; + +exports[`jest-each .test throws error when there are additional words in last column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a | b | expected value + \\"" +`; + +exports[`jest-each .test throws error when there are additional words in second column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a | b is the right | expected + \\"" +`; + exports[`jest-each .test throws error when there are fewer arguments than headings over multiple rows 1`] = ` "Not enough arguments supplied for given headings: a | b | expected @@ -278,6 +530,42 @@ exports[`jest-each .test.concurrent throws an error when called with an empty st " `; +exports[`jest-each .test.concurrent throws error when there are additional words in first column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a is the left | b | expected + \\"" +`; + +exports[`jest-each .test.concurrent throws error when there are additional words in last column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a | b | expected value + \\"" +`; + +exports[`jest-each .test.concurrent throws error when there are additional words in second column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a | b is the right | expected + \\"" +`; + exports[`jest-each .test.concurrent throws error when there are fewer arguments than headings over multiple rows 1`] = ` "Not enough arguments supplied for given headings: a | b | expected @@ -317,6 +605,42 @@ exports[`jest-each .test.concurrent.only throws an error when called with an emp " `; +exports[`jest-each .test.concurrent.only throws error when there are additional words in first column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a is the left | b | expected + \\"" +`; + +exports[`jest-each .test.concurrent.only throws error when there are additional words in last column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a | b | expected value + \\"" +`; + +exports[`jest-each .test.concurrent.only throws error when there are additional words in second column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a | b is the right | expected + \\"" +`; + exports[`jest-each .test.concurrent.only throws error when there are fewer arguments than headings over multiple rows 1`] = ` "Not enough arguments supplied for given headings: a | b | expected @@ -356,6 +680,42 @@ exports[`jest-each .test.concurrent.skip throws an error when called with an emp " `; +exports[`jest-each .test.concurrent.skip throws error when there are additional words in first column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a is the left | b | expected + \\"" +`; + +exports[`jest-each .test.concurrent.skip throws error when there are additional words in last column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a | b | expected value + \\"" +`; + +exports[`jest-each .test.concurrent.skip throws error when there are additional words in second column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a | b is the right | expected + \\"" +`; + exports[`jest-each .test.concurrent.skip throws error when there are fewer arguments than headings over multiple rows 1`] = ` "Not enough arguments supplied for given headings: a | b | expected @@ -395,6 +755,42 @@ exports[`jest-each .test.only throws an error when called with an empty string 1 " `; +exports[`jest-each .test.only throws error when there are additional words in first column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a is the left | b | expected + \\"" +`; + +exports[`jest-each .test.only throws error when there are additional words in last column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a | b | expected value + \\"" +`; + +exports[`jest-each .test.only throws error when there are additional words in second column heading 1`] = ` +"Table headings do not conform to expected format: + +heading1 | headingN + +Received: + +\\" + a | b is the right | expected + \\"" +`; + exports[`jest-each .test.only throws error when there are fewer arguments than headings over multiple rows 1`] = ` "Not enough arguments supplied for given headings: a | b | expected diff --git a/packages/jest-each/src/__tests__/template.test.ts b/packages/jest-each/src/__tests__/template.test.ts index fe5d67217d0b..e185c52e0886 100644 --- a/packages/jest-each/src/__tests__/template.test.ts +++ b/packages/jest-each/src/__tests__/template.test.ts @@ -52,6 +52,123 @@ describe('jest-each', () => { ['describe', 'only'], ].forEach(keyPath => { describe(`.${keyPath.join('.')}`, () => { + test('throws error when there are additional words in first column heading', () => { + const globalTestMocks = getGlobalTestMocks(); + const eachObject = each.withGlobal(globalTestMocks)` + a is the left | b | expected + ${1} | ${1} | ${2} + `; + const testFunction = get(eachObject, keyPath); + const testCallBack = jest.fn(); + testFunction('this will blow up :(', testCallBack); + + const globalMock = get(globalTestMocks, keyPath); + + expect(() => + globalMock.mock.calls[0][1](), + ).toThrowErrorMatchingSnapshot(); + expect(testCallBack).not.toHaveBeenCalled(); + }); + + test('throws error when there are additional words in second column heading', () => { + const globalTestMocks = getGlobalTestMocks(); + const eachObject = each.withGlobal(globalTestMocks)` + a | b is the right | expected + ${1} | ${1} | ${2} + `; + const testFunction = get(eachObject, keyPath); + const testCallBack = jest.fn(); + testFunction('this will blow up :(', testCallBack); + + const globalMock = get(globalTestMocks, keyPath); + + expect(() => + globalMock.mock.calls[0][1](), + ).toThrowErrorMatchingSnapshot(); + expect(testCallBack).not.toHaveBeenCalled(); + }); + + test('throws error when there are additional words in last column heading', () => { + const globalTestMocks = getGlobalTestMocks(); + const eachObject = each.withGlobal(globalTestMocks)` + a | b | expected value + ${1} | ${1} | ${2} + `; + const testFunction = get(eachObject, keyPath); + const testCallBack = jest.fn(); + testFunction('this will blow up :(', testCallBack); + + const globalMock = get(globalTestMocks, keyPath); + + expect(() => + globalMock.mock.calls[0][1](), + ).toThrowErrorMatchingSnapshot(); + expect(testCallBack).not.toHaveBeenCalled(); + }); + + test('does not throw error when there is additional words in template after heading row', () => { + const globalTestMocks = getGlobalTestMocks(); + const eachObject = each.withGlobal(globalTestMocks)` + a | b | expected + foo + bar + ${1} | ${1} | ${2} + `; + const testFunction = get(eachObject, keyPath); + const testCallBack = jest.fn(); + testFunction('test title', testCallBack); + + const globalMock = get(globalTestMocks, keyPath); + + expect(globalMock).toHaveBeenCalledTimes(1); + expect(globalMock).toHaveBeenCalledWith( + 'test title', + expectFunction, + undefined, + ); + }); + + test('does not throw error when there is only one column', () => { + const globalTestMocks = getGlobalTestMocks(); + const eachObject = each.withGlobal(globalTestMocks)` + a + ${1} + `; + const testFunction = get(eachObject, keyPath); + const testCallBack = jest.fn(); + testFunction('test title', testCallBack); + + const globalMock = get(globalTestMocks, keyPath); + + expect(globalMock).toHaveBeenCalledTimes(1); + expect(globalMock).toHaveBeenCalledWith( + 'test title', + expectFunction, + undefined, + ); + }); + + test('does not throw error when there is only one column with additional words in template after heading', () => { + const globalTestMocks = getGlobalTestMocks(); + const eachObject = each.withGlobal(globalTestMocks)` + a + hello world + ${1} + `; + const testFunction = get(eachObject, keyPath); + const testCallBack = jest.fn(); + testFunction('test title $a', testCallBack); + + const globalMock = get(globalTestMocks, keyPath); + + expect(globalMock).toHaveBeenCalledTimes(1); + expect(globalMock).toHaveBeenCalledWith( + 'test title 1', + expectFunction, + undefined, + ); + }); + test('throws error when there are no arguments for given headings', () => { const globalTestMocks = getGlobalTestMocks(); const eachObject = each.withGlobal(globalTestMocks)` diff --git a/packages/jest-each/src/bind.ts b/packages/jest-each/src/bind.ts index ba4ae6abcaa0..0e453f385536 100644 --- a/packages/jest-each/src/bind.ts +++ b/packages/jest-each/src/bind.ts @@ -10,7 +10,11 @@ import type {Global} from '@jest/types'; import {ErrorWithStack} from 'jest-util'; import convertArrayTable from './table/array'; import convertTemplateTable from './table/template'; -import {validateArrayTable, validateTemplateTableHeadings} from './validation'; +import { + extractValidTemplateHeadings, + validateArrayTable, + validateTemplateTableArguments, +} from './validation'; export type EachTests = Array<{ title: string; @@ -66,12 +70,12 @@ const buildTemplateTests = ( taggedTemplateData: Global.TemplateData, ): EachTests => { const headings = getHeadingKeys(table[0] as string); - validateTemplateTableHeadings(headings, taggedTemplateData); + validateTemplateTableArguments(headings, taggedTemplateData); return convertTemplateTable(title, headings, taggedTemplateData); }; const getHeadingKeys = (headings: string): Array => - headings.replace(/\s/g, '').split('|'); + extractValidTemplateHeadings(headings).replace(/\s/g, '').split('|'); const applyArguments = ( supportsDone: boolean, diff --git a/packages/jest-each/src/validation.ts b/packages/jest-each/src/validation.ts index 3cd70e143127..5b7e5a9eee3a 100644 --- a/packages/jest-each/src/validation.ts +++ b/packages/jest-each/src/validation.ts @@ -50,7 +50,7 @@ const isEmptyTable = (table: Array) => table.length === 0; const isEmptyString = (str: string | unknown) => typeof str === 'string' && str.trim() === ''; -export const validateTemplateTableHeadings = ( +export const validateTemplateTableArguments = ( headings: Array, data: TemplateData, ): void => { @@ -74,3 +74,28 @@ export const validateTemplateTableHeadings = ( const pluralize = (word: string, count: number) => word + (count === 1 ? '' : 's'); + +const START_OF_LINE = '^'; +const NEWLINE = '\\n'; +const HEADING = '\\s*\\w+\\s*'; +const PIPE = '\\|'; +const REPEATABLE_HEADING = `(${PIPE}${HEADING})*`; +const HEADINGS_FORMAT = new RegExp( + START_OF_LINE + NEWLINE + HEADING + REPEATABLE_HEADING + NEWLINE, + 'g', +); + +export const extractValidTemplateHeadings = (headings: string): string => { + const matches = headings.match(HEADINGS_FORMAT); + if (matches === null) { + throw new Error( + 'Table headings do not conform to expected format:\n\n' + + EXPECTED_COLOR('heading1 | headingN') + + '\n\n' + + 'Received:\n\n' + + RECEIVED_COLOR(pretty(headings)), + ); + } + + return matches[0]; +};