diff --git a/package-lock.json b/package-lock.json index 6a1da1e..6b9e045 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2514,8 +2514,7 @@ "@types/node": { "version": "13.13.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.0.tgz", - "integrity": "sha512-WE4IOAC6r/yBZss1oQGM5zs2D7RuKR6Q+w+X2SouPofnWn+LbCqClRyhO3ZE7Ix8nmFgo/oVuuE01cJT2XB13A==", - "dev": true + "integrity": "sha512-WE4IOAC6r/yBZss1oQGM5zs2D7RuKR6Q+w+X2SouPofnWn+LbCqClRyhO3ZE7Ix8nmFgo/oVuuE01cJT2XB13A==" }, "@types/normalize-package-data": { "version": "2.4.0", @@ -3610,12 +3609,12 @@ } }, "call-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", - "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", "requires": { "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.0" + "get-intrinsic": "^1.0.2" } }, "callsites": { @@ -3934,9 +3933,9 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" }, "core-js": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.1.tgz", - "integrity": "sha512-9Id2xHY1W7m8hCl8NkhQn5CufmF/WuR30BTRewvCXc1aZd3kMECwNZ69ndLbekKfakw9Rf2Xyc+QR6E7Gg+obg==" + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.8.2.tgz", + "integrity": "sha512-FfApuSRgrR6G5s58casCBd9M2k+4ikuu4wbW6pJyYU7bd9zvFc9qf7vr5xmrZOhT9nn+8uwlH1oRR9jTnFoA3A==" }, "core-util-is": { "version": "1.0.2", @@ -4428,21 +4427,24 @@ } }, "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "version": "1.18.0-next.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.2.tgz", + "integrity": "sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw==", "requires": { + "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2", "has": "^1.0.3", "has-symbols": "^1.0.1", "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.1", "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", + "object-inspect": "^1.9.0", "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.3", + "string.prototype.trimstart": "^1.0.3" } }, "es-to-primitive": { @@ -4924,6 +4926,14 @@ "readable-stream": "^2.3.6" } }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "requires": { + "is-callable": "^1.1.3" + } + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -6438,16 +6448,89 @@ } }, "jest-editor-support": { - "version": "28.0.0", - "resolved": "https://registry.npmjs.org/jest-editor-support/-/jest-editor-support-28.0.0.tgz", - "integrity": "sha512-Q4dc96HI0Y9eBkN0RwZL4PEZhKU4VHOKetsu1AsuNe+aypV44p2wa9RrYgZN0JQQ6FSyV00cSX/2BfPiOsutNw==", + "version": "28.1.0", + "resolved": "https://registry.npmjs.org/jest-editor-support/-/jest-editor-support-28.1.0.tgz", + "integrity": "sha512-h6Afk3+B+30HHq/UBmJdRqVC7B6rzw9lmFnlEvoKe2Bq73+Cr9FxuxSzi/znezi97nmVuS0AMcynFkemk9gmkg==", "requires": { "@babel/parser": "^7.8.3", "@babel/traverse": "^7.6.2", "@babel/types": "^7.8.3", - "@jest/types": "^24.8.0", + "@jest/types": "^26.6.2", "core-js": "^3.2.1", "jest-snapshot": "^24.7.0" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/yargs": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.12.tgz", + "integrity": "sha512-f+fD/fQAo3BCbCDlrUpznF1A5Zp9rB0noS5vnoormHSIPFKL0Z2DcUJ3Gxp5ytH4uLRNxy7AwYUC9exZzqGMAw==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + } } }, "jest-environment-jsdom": { @@ -9571,27 +9654,6 @@ "call-bind": "^1.0.0", "define-properties": "^1.1.3", "es-abstract": "^1.18.0-next.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.18.0-next.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", - "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-negative-zero": "^2.0.0", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } } }, "object.pick": { @@ -11662,14 +11724,15 @@ "dev": true }, "util.promisify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", - "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.1.1.tgz", + "integrity": "sha512-/s3UsZUrIfa6xDhr7zZhnE9SLQ5RIXyYfiVnMMyMDzOc8WhWN4Nbh36H842OyurKbCDAesZOJaVyvmSl6fhGQw==", "requires": { + "call-bind": "^1.0.0", "define-properties": "^1.1.3", - "es-abstract": "^1.17.2", + "for-each": "^0.3.3", "has-symbols": "^1.0.1", - "object.getownpropertydescriptors": "^2.1.0" + "object.getownpropertydescriptors": "^2.1.1" } }, "uuid": { diff --git a/package.json b/package.json index aa4b5dc..515b8cf 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@babel/traverse": "^7.7.4", "@schemastore/package": "0.0.5", "cosmiconfig": "^6.0.0", - "jest-editor-support": "^28.0.0", + "jest-editor-support": "^28.1.0", "lodash": "^4.17.15", "micromatch": "^4.0.2", "semver": "^7.3.2", diff --git a/src/adapter.ts b/src/adapter.ts index 78ea4c4..913a7c7 100644 --- a/src/adapter.ts +++ b/src/adapter.ts @@ -217,7 +217,7 @@ export default class JestTestAdapter implements TestAdapter { const testFilter = mapTestIdsToTestFilter(testsToRun); // we emit events to notify which tests we are running. - const filteredTree = filterTree(project, testsToRun); + const filteredTree = filterTree(project, testsToRun, false); emitTestRunningRootNode(filteredTree, eventEmitter); // begin running the tests in Jest. @@ -232,7 +232,7 @@ export default class JestTestAdapter implements TestAdapter { const treeWithRuntime = mergeRuntimeResults(project, jestResponse.results.testResults); // filter the tree - const filteredTreeWithRuntime = filterTree(treeWithRuntime, testsToRun); + const filteredTreeWithRuntime = filterTree(treeWithRuntime, testsToRun, true); const testEvents = mapJestTestResultsToTestEvents(jestResponse, filteredTreeWithRuntime); emitTestCompleteRootNode(filteredTreeWithRuntime, testEvents, eventEmitter); diff --git a/src/helpers/__tests__/escapeRegExp.test.ts b/src/helpers/__tests__/escapeRegExp.test.ts new file mode 100644 index 0000000..c955ac9 --- /dev/null +++ b/src/helpers/__tests__/escapeRegExp.test.ts @@ -0,0 +1,308 @@ +import { replacePrintfPatterns } from '../escapeRegExp'; + +const POSITIVE_WHOLE_NUMBERS = [ + '1', + '123908124' +]; +const NEGATIVE_WHOLE_NUMBERS = [ + '-1', + '-123908124' +]; +const SPECIAL_NUMBERS = [ + 'NaN', + 'Infinity', + '-Infinity' +]; +const DECIMALS = [ + '0.123', + '1123.1', + '-0.0', + '-1.1241', + '-123.1412' +]; +const ZERO = '0'; +const NEGATIVE_ZERO = '-0'; +const NOTHING = ''; +const JSON_OBJECTS = [ + {"key":"value"}, + {"key":{"subKey":"value"}}, + {"key":[0,1]}, + {"key":[{"subKey":"value"}]} +].map(obj => JSON.stringify(obj)); +const JSON_ARRAYS = [ + [1,2,3], + ["a","b","c"], + [{"key":"value"}], + [{"key":{"subKey":"value"}}], + [{"key":[0,1]}] +].map(ary => JSON.stringify(ary)); +const JSON_STRINGS = [ + 'someString', + 'some"escaped' +].map(str => JSON.stringify(str)); +const OBVIOUS_INVALID_JSON_STRINGS = [ + 'stringWithoutQuotes', + 'string without quotes', + '"unclosed string', + "'string in single-quotes'" +]; +const NON_NUMERICS = [ + ...JSON_OBJECTS, + ...JSON_ARRAYS, + ...JSON_STRINGS, + ...OBVIOUS_INVALID_JSON_STRINGS +]; +const PRINTF_PATTERNS = [ + '%#', + '%i', + '%d', + '%f', + '%j', + '%p', + '%s', + '%o', + '%%', + '%c' +]; + +describe('replacePrintfPatterns', () => { + describe('replaces %#', () => { + const placeholder = '%#'; + const pattern = replacePrintfPatterns(placeholder); + const regex = new RegExp(`^${pattern}$`); + + it('matches itself', () => { + expect(regex.test(placeholder)).toBeTrue(); + }); + + it.each(PRINTF_PATTERNS.filter(otherPlaceholder => otherPlaceholder !== placeholder))('does NOT match other printf patterns (%s)', (otherPlaceholder) => { + expect(regex.test(otherPlaceholder)).toBeFalse(); + }); + + it.each(POSITIVE_WHOLE_NUMBERS)('matches positive whole numbers (%s)', (value) => { + expect(regex.test(value)).toBeTrue(); + }); + + it.each(NEGATIVE_WHOLE_NUMBERS)('does NOT match negative numbers (%s)', (value) => { + expect(regex.test(value)).toBeFalse(); + }); + + it('matches zero', () => { + expect(regex.test(ZERO)).toBeTrue(); + }); + + it('does NOT match negative zero', () => { + expect(regex.test(NEGATIVE_ZERO)).toBeFalse(); + }); + + it.each(SPECIAL_NUMBERS)('does NOT match special numbers (%s)', (value) => { + expect(regex.test(value)).toBeFalse(); + }); + + it.each(DECIMALS)('does NOT match decimals (%s)', (value) => { + expect(regex.test(value)).toBeFalse(); + }); + + it.each(NON_NUMERICS)('does NOT match non-numerics (%s)', (value) => { + expect(regex.test(value)).toBeFalse(); + }); + + it('does NOT match nothing (%s)', () => { + expect(regex.test(NOTHING)).toBeFalse(); + }); + }); + + describe('replaces %i', () => { + const placeholder = '%i'; + const pattern = replacePrintfPatterns(placeholder); + const regex = new RegExp(`^${pattern}$`); + + it('matches itself', () => { + expect(regex.test(placeholder)).toBeTrue(); + }); + + it.each(PRINTF_PATTERNS.filter(otherPlaceholder => otherPlaceholder !== placeholder))('does NOT match other printf patterns (%s)', (otherPlaceholder) => { + expect(regex.test(otherPlaceholder)).toBeFalse(); + }); + + it.each(POSITIVE_WHOLE_NUMBERS)('matches positive whole numbers (%s)', (value) => { + expect(regex.test(value)).toBeTrue(); + }); + + it.each(NEGATIVE_WHOLE_NUMBERS)('matches negative numbers (%s)', (value) => { + expect(regex.test(value)).toBeTrue(); + }); + + it('matches zero', () => { + expect(regex.test(ZERO)).toBeTrue(); + }); + + it('does NOT match negative zero', () => { + expect(regex.test(NEGATIVE_ZERO)).toBeFalse(); + }); + + it.each(SPECIAL_NUMBERS)('matches special numbers (%s)', (value) => { + expect(regex.test(value)).toBeTrue(); + }); + + it.each(DECIMALS)('does NOT match decimals (%s)', (value) => { + expect(regex.test(value)).toBeFalse(); + }); + + it.each(NON_NUMERICS)('does NOT match non-numerics (%s)', (value) => { + expect(regex.test(value)).toBeFalse(); + }); + + it('does NOT match nothing (%s)', () => { + expect(regex.test(NOTHING)).toBeFalse(); + }); + }); + + ['%d', '%f'].forEach(placeholder => describe(`replaces ${placeholder}`, () => { + const pattern = replacePrintfPatterns(placeholder); + const regex = new RegExp(`^${pattern}$`); + + it('matches itself', () => { + expect(regex.test(placeholder)).toBeTrue(); + }); + + it.each(PRINTF_PATTERNS.filter(otherPlaceholder => otherPlaceholder !== placeholder))('does NOT match other printf patterns (%s)', (otherPlaceholder) => { + expect(regex.test(otherPlaceholder)).toBeFalse(); + }); + + it.each(POSITIVE_WHOLE_NUMBERS)('matches positive whole numbers (%s)', (value) => { + expect(regex.test(value)).toBeTrue(); + }); + + it.each(NEGATIVE_WHOLE_NUMBERS)('matches negative numbers (%s)', (value) => { + expect(regex.test(value)).toBeTrue(); + }); + + it('matches zero', () => { + expect(regex.test(ZERO)).toBeTrue(); + }); + + it('matches negative zero', () => { + expect(regex.test(NEGATIVE_ZERO)).toBeTrue(); + }); + + it.each(SPECIAL_NUMBERS)('matches special numbers (%s)', (value) => { + expect(regex.test(value)).toBeTrue(); + }); + + it.each(DECIMALS)('matches decimals (%s)', (value) => { + expect(regex.test(value)).toBeTrue(); + }); + + it.each(NON_NUMERICS)('does NOT match non-numerics (%s)', (value) => { + expect(regex.test(value)).toBeFalse(); + }); + + it('does NOT match nothing (%s)', () => { + expect(regex.test(NOTHING)).toBeFalse(); + }); + })); + + ['%p', '%s', '%o'].forEach(placeholder => describe(`replaces ${placeholder}`, () => { + const pattern = replacePrintfPatterns(placeholder); + const regex = new RegExp(`^${pattern}$`); + + it('matches itself', () => { + expect(regex.test(placeholder)).toBeTrue(); + }); + + it.each(PRINTF_PATTERNS.filter(otherPlaceholder => otherPlaceholder !== placeholder))('matches other printf patterns (%s)', (otherPlaceholder) => { + expect(regex.test(otherPlaceholder)).toBeTrue(); + }); + + it.each(POSITIVE_WHOLE_NUMBERS)('matches positive whole numbers (%s)', (value) => { + expect(regex.test(value)).toBeTrue(); + }); + + it.each(NEGATIVE_WHOLE_NUMBERS)('matches negative numbers (%s)', (value) => { + expect(regex.test(value)).toBeTrue(); + }); + + it('matches zero', () => { + expect(regex.test(ZERO)).toBeTrue(); + }); + + it('matches negative zero', () => { + expect(regex.test(NEGATIVE_ZERO)).toBeTrue(); + }); + + it.each(SPECIAL_NUMBERS)('matches special numbers (%s)', (value) => { + expect(regex.test(value)).toBeTrue(); + }); + + it.each(DECIMALS)('matches match decimals (%s)', (value) => { + expect(regex.test(value)).toBeTrue(); + }); + + it.each(NON_NUMERICS)('matches non-numerics (%s)', (value) => { + expect(regex.test(value)).toBeTrue(); + }); + + it('matches nothing (%s)', () => { + expect(regex.test(NOTHING)).toBeTrue(); + }); + })); + + describe('replaces %j', () => { + const placeholder = '%j'; + const pattern = replacePrintfPatterns(placeholder); + const regex = new RegExp(`^${pattern}$`); + + it('matches itself', () => { + expect(regex.test(placeholder)).toBeTrue(); + }); + + it.each(PRINTF_PATTERNS.filter(otherPlaceholder => otherPlaceholder !== placeholder))('does NOT match other printf patterns (%s)', (otherPlaceholder) => { + expect(regex.test(otherPlaceholder)).toBeFalse(); + }); + + it.each(POSITIVE_WHOLE_NUMBERS)('matches positive whole numbers (%s)', (value) => { + expect(regex.test(value)).toBeTrue(); + }); + + it.each(NEGATIVE_WHOLE_NUMBERS)('matches negative numbers (%s)', (value) => { + expect(regex.test(value)).toBeTrue(); + }); + + it('matches zero', () => { + expect(regex.test(ZERO)).toBeTrue(); + }); + + it('matches negative zero', () => { + expect(regex.test(NEGATIVE_ZERO)).toBeTrue(); + }); + + it.each(SPECIAL_NUMBERS)('matches special numbers (%s)', (value) => { + expect(regex.test(value)).toBeTrue(); + }); + + it.each(DECIMALS)('matches match decimals (%s)', (value) => { + expect(regex.test(value)).toBeTrue(); + }); + + it.each(JSON_OBJECTS)('matches objects (%s)', (value) => { + expect(regex.test(value)).toBeTrue(); + }); + + it.each(JSON_ARRAYS)('matches arrays (%s)', (value) => { + expect(regex.test(value)).toBeTrue(); + }); + + it.each(JSON_STRINGS)('matches strings (%s)', (value) => { + expect(regex.test(value)).toBeTrue(); + }); + + it.each(OBVIOUS_INVALID_JSON_STRINGS)('does NOT match very obvious invalid strings (%s)', (value) => { + expect(regex.test(value)).toBeFalse(); + }); + + it('does NOT match nothing (%s)', () => { + expect(regex.test(NOTHING)).toBeFalse(); + }); + }); +}); \ No newline at end of file diff --git a/src/helpers/__tests__/filterTree.test.ts b/src/helpers/__tests__/filterTree.test.ts new file mode 100644 index 0000000..48352de --- /dev/null +++ b/src/helpers/__tests__/filterTree.test.ts @@ -0,0 +1,1413 @@ +import { flatMap, flattenDeep } from 'lodash'; +// import { mapTestIdToTestNamePattern } from "../mapTestIdsToTestFilter"; +import { + createDescribeNode, + createFileNode, + // createFolderNode, + createProjectNode, + createTestNode, + DescribeNode, + FileNode, + FileWithParseErrorNode, + // FileWithParseErrorNode, + FolderNode, + ProjectRootNode, + TestNode, +} from "../tree"; +import { ProjectConfig } from '../../repo'; +import { Id, mapIdToString } from '../idMaps'; +import { filterTree } from '../filterTree'; + +const PROJECT_NAME = 'mock-project'; +const ROOT_PATH = `/${PROJECT_NAME}`; +const dummyConfig: ProjectConfig = { + jestCommand: "", + jestConfig: "", + jestExecutionDirectory: "", + projectName: PROJECT_NAME, + rootPath: ROOT_PATH, + tsConfig: "", +}; + +const isFolder = (object: FileNode | FolderNode): object is FolderNode => { + return 'folders' in object || 'files' in object; +}; + +const isFile = (object: FileNode | FolderNode): object is FileNode => { + return !isFolder(object); +}; + +const isDescribe = (object: TestNode | DescribeNode): object is DescribeNode => { + return 'tests' in object || 'describeBlocks' in object; +}; + +const isTest = (object: TestNode | DescribeNode): object is TestNode => { + return !isDescribe(object); +}; + +const createProject = (id: Id, createChildren: (id: Id) => (FileNode | FolderNode)[]): ProjectRootNode => { + const project = createProjectNode(PROJECT_NAME, PROJECT_NAME, dummyConfig); + const children = createChildren(id); + project.folders = children.filter(isFolder); + project.files = children.filter(isFile); + return project; +}; + +// const createFolder = (rootId: Id, folderName: string, createChildren: (id:Id) => (FileNode|FolderNode)[]): FolderNode => { +// const id: Id = { +// ...rootId, +// fileName: `${rootId.fileName || ''}/${folderName}` +// }; +// const idString = mapIdToString(id); + +// const folder = createFolderNode(idString, folderName); +// const children = createChildren(id); +// folder.folders = children.filter(isFolder); +// folder.files = children.filter(isFile); + +// return folder; +// }; + +const createFile = (rootId: Id, fileName: string, createDescribeBlocks: (id: Id) => DescribeNode[]): FileNode => { + const id = { + ...rootId, + fileName: `${rootId.fileName || ''}/${fileName}` + }; + const idString = mapIdToString(id); + + const file = createFileNode(idString, fileName, id.fileName); + file.describeBlocks = createDescribeBlocks(id); + + return file; +}; + +const createDescribe = (rootId: Id, label: string, line: number, createChildren: (id: Id) => (TestNode | DescribeNode)[] = (id) => []): DescribeNode => { + const id = { + ...rootId, + describeIds: (rootId.describeIds || []).concat(label) + }; + const idString = mapIdToString(id); + const fileName = (id.fileName || ''); + + const describeNode = createDescribeNode(idString, label, fileName, line, false); + const children = createChildren(id); + describeNode.describeBlocks = children.filter(isDescribe); + describeNode.tests = children.filter(isTest); + + return describeNode; +}; + +const createTest = (rootId: Id, label: string, line: number): TestNode => { + const id = { + ...rootId, + testId: label + }; + const idString = mapIdToString(id); + const fileName = (id.fileName || ''); + + return createTestNode(idString, label, fileName, line, false); +} + +describe('filterTree', () => { + describe('using plan string matches', () => { + describe('tree with single test', () => { + const BASE_ID = { projectId: PROJECT_NAME, fileName: ROOT_PATH }; + const tree = createProject(BASE_ID, id => [ + createFile(id, 'some-file.js', id => [ + createDescribe(id, 'someDescribe', 1, id => [ + createDescribe(id, 'innerDescribe', 1, id => [ + createTest(id, 'someTest', 2) + ]) + ]) + ]) + ]); + + describe('expected matches', () => { + const expectedFilteredTree = { + files: [{ + describeBlocks: [{ + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [{ label: "someTest" }] + }], + label: "someDescribe", + tests: [] + }], + file: "/mock-project/some-file.js" + }], + folders: [], + label: "mock-project" + }; + + it('given full test id, matches test', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe', 'innerDescribe'], + testId: 'someTest' + }) + ]; + + const filteredTree = filterTree(tree, testNames, false); + + expect(filteredTree).toMatchObject(expectedFilteredTree); + }); + + it('given nested describe id, matches test', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe', 'innerDescribe'] + }) + ]; + + const filteredTree = filterTree(tree, testNames, false); + + expect(filteredTree).toMatchObject(expectedFilteredTree); + }); + + it('given top-level describe id, matches test', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe'] + }) + ]; + + const filteredTree = filterTree(tree, testNames, false); + + expect(filteredTree).toMatchObject(expectedFilteredTree); + }); + + it('given file id, matches test', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js` + }) + ]; + + const filteredTree = filterTree(tree, testNames, false); + + expect(filteredTree).toMatchObject(expectedFilteredTree); + }); + + it('given project id, matches test', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME + }) + ]; + + const filteredTree = filterTree(tree, testNames, false); + + expect(filteredTree).toMatchObject(expectedFilteredTree); + }); + }); + + describe('expected misses', () => { + it('given different test id, matches describes, but not test', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe', 'innerDescribe'], + testId: 'differentTest' + }) + ]; + + const filteredTree = filterTree(tree, testNames, false); + + const allDescribesButNoTest = { + files: [{ + describeBlocks: [{ + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [ + // No test found + ] + }], + label: "someDescribe", + tests: [] + }], + file: "/mock-project/some-file.js" + }], + folders: [], + label: "mock-project" + }; + expect(filteredTree).toMatchObject(allDescribesButNoTest); + }); + + // Currently failing + it('given test id plus suffix, matches describes, but not test', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe', 'innerDescribe'], + testId: 'someTest2' + }) + ]; + + const filteredTree = filterTree(tree, testNames, false); + + const allDescribesButNoTest = { + files: [{ + describeBlocks: [{ + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [ + // No test found + ] + }], + label: "someDescribe", + tests: [] + }], + file: "/mock-project/some-file.js" + }], + folders: [], + label: "mock-project" + }; + expect(filteredTree).toMatchObject(allDescribesButNoTest); + }); + + it('given correct test, but wrong inner describe, matches file and outer describe only', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe', 'otherDescribe'], + testId: 'someTest' + }) + ]; + + const filteredTree = filterTree(tree, testNames, false); + + const outerDescribeOnly = { + files: [{ + describeBlocks: [{ + describeBlocks: [ + // No inner describe + ], + label: "someDescribe", + tests: [] + }], + file: "/mock-project/some-file.js" + }], + folders: [], + label: "mock-project" + }; + expect(filteredTree).toMatchObject(outerDescribeOnly); + }); + + // Currently failing + it('given correct test, but inner describe with suffix, matches file and outer describe only', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe', 'innerDescribe2'], + testId: 'someTest' + }) + ]; + + const filteredTree = filterTree(tree, testNames, false); + + const outerDescribeOnly = { + files: [{ + describeBlocks: [{ + describeBlocks: [ + // No inner describe + ], + label: "someDescribe", + tests: [] + }], + file: "/mock-project/some-file.js" + }], + folders: [], + label: "mock-project" + }; + expect(filteredTree).toMatchObject(outerDescribeOnly); + }); + + it('given correct test, but wrong outer describe, matches file only', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['otherDescribe', 'innerDescribe'], + testId: 'someTest' + }) + ]; + + const filteredTree = filterTree(tree, testNames, false); + + const fileOnly = { + files: [{ + describeBlocks: [ + // No inner describe + ], + file: "/mock-project/some-file.js" + }], + folders: [], + label: "mock-project" + }; + expect(filteredTree).toMatchObject(fileOnly); + }); + + // Currently failing + it('given correct test, but outer describe with suffix, matches file only', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe2', 'innerDescribe'], + testId: 'someTest' + }) + ]; + + const filteredTree = filterTree(tree, testNames, false); + + const fileOnly = { + files: [{ + describeBlocks: [ + // No inner describe + ], + file: "/mock-project/some-file.js" + }], + folders: [], + label: "mock-project" + }; + expect(filteredTree).toMatchObject(fileOnly); + }); + }); + }); + + describe('tree with multiple options at each level', () => { + const BASE_ID = { projectId: PROJECT_NAME, fileName: ROOT_PATH }; + const tree = createProject(BASE_ID, id => [ + createFile(id, 'some-file.js', id => [ + createDescribe(id, 'someDescribe', 1, id => [ + createDescribe(id, 'innerDescribe', 1, id => [ + createTest(id, 'someTest', 2), + createTest(id, 'someTest2', 2), + createTest(id, 'otherTest', 2) + ]), + createDescribe(id, 'innerDescribe2', 1, id => [ + createTest(id, 'someTest', 2) + ]), + createDescribe(id, 'otherInnerDescribe', 1, id => [ + createTest(id, 'someTest', 2) + ]) + ]), + createDescribe(id, 'someDescribe2', 1, id => [ + createDescribe(id, 'innerDescribe', 1, id => [ + createTest(id, 'someTest', 2) + ]) + ]), + createDescribe(id, 'otherDescribe', 1, id => [ + createDescribe(id, 'innerDescribe', 1, id => [ + createTest(id, 'someTest', 2) + ]) + ]) + ]), + createFile(id, 'some-other-file.js', id => [ + createDescribe(id, 'someDescribe', 1, id => [ + createDescribe(id, 'innerDescribe', 1, id => [ + createTest(id, 'someTest', 2) + ]) + ]) + ]) + ]); + + describe('expected matches', () => { + it('given full test id, matches only specific test', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe', 'innerDescribe'], + testId: 'someTest' + }) + ]; + + const filteredTree = filterTree(tree, testNames, false); + + expect(filteredTree).toMatchObject({ + files: [{ + describeBlocks: [{ + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [{ label: "someTest" }] + }], + label: "someDescribe", + tests: [] + }], + file: "/mock-project/some-file.js" + }], + folders: [], + label: "mock-project" + }); + }); + + it('given nested describe id, matches all tests in nested describe', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe', 'innerDescribe'] + }) + ]; + + const filteredTree = filterTree(tree, testNames, false); + + expect(filteredTree).toMatchObject({ + files: [{ + describeBlocks: [{ + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [ + { label: "someTest" }, + { label: "someTest2" }, + { label: "otherTest" } + ] + }], + label: "someDescribe", + tests: [] + }], + file: "/mock-project/some-file.js" + }], + folders: [], + label: "mock-project" + }); + }); + + it('given top-level describe id, matches all tests in outer describe', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe'] + }) + ]; + + const filteredTree = filterTree(tree, testNames, false); + + expect(filteredTree).toMatchObject({ + files: [{ + describeBlocks: [{ + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [ + { label: "someTest" }, + { label: "someTest2" }, + { label: "otherTest" } + ] + }, { + describeBlocks: [], + label: "innerDescribe2", + tests: [ + { label: "someTest" } + ] + }, { + describeBlocks: [], + label: "otherInnerDescribe", + tests: [ + { label: "someTest" } + ] + }], + label: "someDescribe", + tests: [] + }], + file: "/mock-project/some-file.js" + }], + folders: [], + label: "mock-project" + }); + }); + + it('given file id, matches all tests in file', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js` + }) + ]; + + const filteredTree = filterTree(tree, testNames, false); + + expect(filteredTree).toMatchObject({ + files: [{ + describeBlocks: [{ + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [ + { label: "someTest" }, + { label: "someTest2" }, + { label: "otherTest" } + ] + }, { + describeBlocks: [], + label: "innerDescribe2", + tests: [ + { label: "someTest" } + ] + }, { + describeBlocks: [], + label: "otherInnerDescribe", + tests: [ + { label: "someTest" } + ] + }], + label: "someDescribe", + tests: [] + }, { + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [ + { label: "someTest" } + ] + }], + label: "someDescribe2", + tests: [] + }, { + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [ + { label: "someTest" } + ] + }], + label: "otherDescribe", + tests: [] + }], + file: "/mock-project/some-file.js" + }], + folders: [], + label: "mock-project" + }); + }); + + it('given project id, matches all tests in project', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME + }) + ]; + + const filteredTree = filterTree(tree, testNames, false); + + expect(filteredTree).toMatchObject({ + files: [{ + describeBlocks: [{ + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [ + { label: "someTest" }, + { label: "someTest2" }, + { label: "otherTest" } + ] + }, { + describeBlocks: [], + label: "innerDescribe2", + tests: [ + { label: "someTest" } + ] + }, { + describeBlocks: [], + label: "otherInnerDescribe", + tests: [ + { label: "someTest" } + ] + }], + label: "someDescribe", + tests: [] + }, { + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [ + { label: "someTest" } + ] + }], + label: "someDescribe2", + tests: [] + }, { + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [ + { label: "someTest" } + ] + }], + label: "otherDescribe", + tests: [] + }], + file: "/mock-project/some-file.js" + }, { + describeBlocks: [{ + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [ + { label: "someTest" } + ] + }], + label: "someDescribe", + tests: [] + }], + file: "/mock-project/some-other-file.js" + }], + folders: [], + label: "mock-project" + }); + }); + }); + }); + }); + + describe('using regex matches', () => { + describe('tree with single test', () => { + const BASE_ID = { projectId: PROJECT_NAME, fileName: ROOT_PATH }; + const tree = createProject(BASE_ID, id => [ + createFile(id, 'some-file.js', id => [ + createDescribe(id, 'someDescribe', 1, id => [ + createDescribe(id, 'innerDescribe', 1, id => [ + createTest(id, 'someTest', 2) + ]) + ]) + ]) + ]); + + describe('expected matches', () => { + const expectedFilteredTree = { + files: [{ + describeBlocks: [{ + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [{ label: "someTest" }] + }], + label: "someDescribe", + tests: [] + }], + file: "/mock-project/some-file.js" + }], + folders: [], + label: "mock-project" + }; + + it('given full test id, matches test', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe', 'innerDescribe'], + testId: 'someTest' + }) + ]; + + const filteredTree = filterTree(tree, testNames, true); + + expect(filteredTree).toMatchObject(expectedFilteredTree); + }); + + it('given nested describe id, matches test', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe', 'innerDescribe'] + }) + ]; + + const filteredTree = filterTree(tree, testNames, true); + + expect(filteredTree).toMatchObject(expectedFilteredTree); + }); + + it('given top-level describe id, matches test', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe'] + }) + ]; + + const filteredTree = filterTree(tree, testNames, true); + + expect(filteredTree).toMatchObject(expectedFilteredTree); + }); + + it('given file id, matches test', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js` + }) + ]; + + const filteredTree = filterTree(tree, testNames, true); + + expect(filteredTree).toMatchObject(expectedFilteredTree); + }); + + it('given project id, matches test', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME + }) + ]; + + const filteredTree = filterTree(tree, testNames, true); + + expect(filteredTree).toMatchObject(expectedFilteredTree); + }); + }); + + describe('expected misses', () => { + it('given different test id, matches describes, but not test', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe', 'innerDescribe'], + testId: 'differentTest' + }) + ]; + + const filteredTree = filterTree(tree, testNames, true); + + const allDescribesButNoTest = { + files: [{ + describeBlocks: [{ + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [ + // No test found + ] + }], + label: "someDescribe", + tests: [] + }], + file: "/mock-project/some-file.js" + }], + folders: [], + label: "mock-project" + }; + expect(filteredTree).toMatchObject(allDescribesButNoTest); + }); + + // Currently failing + it('given test id plus suffix, matches describes, but not test', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe', 'innerDescribe'], + testId: 'someTest2' + }) + ]; + + const filteredTree = filterTree(tree, testNames, true); + + const allDescribesButNoTest = { + files: [{ + describeBlocks: [{ + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [ + // No test found + ] + }], + label: "someDescribe", + tests: [] + }], + file: "/mock-project/some-file.js" + }], + folders: [], + label: "mock-project" + }; + expect(filteredTree).toMatchObject(allDescribesButNoTest); + }); + + it('given correct test, but wrong inner describe, matches file and outer describe only', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe', 'otherDescribe'], + testId: 'someTest' + }) + ]; + + const filteredTree = filterTree(tree, testNames, true); + + const outerDescribeOnly = { + files: [{ + describeBlocks: [{ + describeBlocks: [ + // No inner describe + ], + label: "someDescribe", + tests: [] + }], + file: "/mock-project/some-file.js" + }], + folders: [], + label: "mock-project" + }; + expect(filteredTree).toMatchObject(outerDescribeOnly); + }); + + // Currently failing + it('given correct test, but inner describe with suffix, matches file and outer describe only', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe', 'innerDescribe2'], + testId: 'someTest' + }) + ]; + + const filteredTree = filterTree(tree, testNames, true); + + const outerDescribeOnly = { + files: [{ + describeBlocks: [{ + describeBlocks: [ + // No inner describe + ], + label: "someDescribe", + tests: [] + }], + file: "/mock-project/some-file.js" + }], + folders: [], + label: "mock-project" + }; + expect(filteredTree).toMatchObject(outerDescribeOnly); + }); + + it('given correct test, but wrong outer describe, matches file only', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['otherDescribe', 'innerDescribe'], + testId: 'someTest' + }) + ]; + + const filteredTree = filterTree(tree, testNames, true); + + const fileOnly = { + files: [{ + describeBlocks: [ + // No inner describe + ], + file: "/mock-project/some-file.js" + }], + folders: [], + label: "mock-project" + }; + expect(filteredTree).toMatchObject(fileOnly); + }); + + // Currently failing + it('given correct test, but outer describe with suffix, matches file only', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe2', 'innerDescribe'], + testId: 'someTest' + }) + ]; + + const filteredTree = filterTree(tree, testNames, true); + + const fileOnly = { + files: [{ + describeBlocks: [ + // No inner describe + ], + file: "/mock-project/some-file.js" + }], + folders: [], + label: "mock-project" + }; + expect(filteredTree).toMatchObject(fileOnly); + }); + }); + }); + + describe('tree with multiple options at each level', () => { + const BASE_ID = { projectId: PROJECT_NAME, fileName: ROOT_PATH }; + const tree = createProject(BASE_ID, id => [ + createFile(id, 'some-file.js', id => [ + createDescribe(id, 'someDescribe', 1, id => [ + createDescribe(id, 'innerDescribe', 1, id => [ + createTest(id, 'someTest', 2), + createTest(id, 'someTest2', 2), + createTest(id, 'otherTest', 2) + ]), + createDescribe(id, 'innerDescribe2', 1, id => [ + createTest(id, 'someTest', 2) + ]), + createDescribe(id, 'otherInnerDescribe', 1, id => [ + createTest(id, 'someTest', 2) + ]) + ]), + createDescribe(id, 'someDescribe2', 1, id => [ + createDescribe(id, 'innerDescribe', 1, id => [ + createTest(id, 'someTest', 2) + ]) + ]), + createDescribe(id, 'otherDescribe', 1, id => [ + createDescribe(id, 'innerDescribe', 1, id => [ + createTest(id, 'someTest', 2) + ]) + ]) + ]), + createFile(id, 'some-other-file.js', id => [ + createDescribe(id, 'someDescribe', 1, id => [ + createDescribe(id, 'innerDescribe', 1, id => [ + createTest(id, 'someTest', 2) + ]) + ]) + ]) + ]); + + describe('expected matches', () => { + it('given full test id, matches only specific test', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe', 'innerDescribe'], + testId: 'someTest' + }) + ]; + + const filteredTree = filterTree(tree, testNames, true); + + expect(filteredTree).toMatchObject({ + files: [{ + describeBlocks: [{ + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [{ label: "someTest" }] + }], + label: "someDescribe", + tests: [] + }], + file: "/mock-project/some-file.js" + }], + folders: [], + label: "mock-project" + }); + }); + + it('given nested describe id, matches all tests in nested describe', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe', 'innerDescribe'] + }) + ]; + + const filteredTree = filterTree(tree, testNames, true); + + expect(filteredTree).toMatchObject({ + files: [{ + describeBlocks: [{ + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [ + { label: "someTest" }, + { label: "someTest2" }, + { label: "otherTest" } + ] + }], + label: "someDescribe", + tests: [] + }], + file: "/mock-project/some-file.js" + }], + folders: [], + label: "mock-project" + }); + }); + + it('given top-level describe id, matches all tests in outer describe', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe'] + }) + ]; + + const filteredTree = filterTree(tree, testNames, true); + + expect(filteredTree).toMatchObject({ + files: [{ + describeBlocks: [{ + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [ + { label: "someTest" }, + { label: "someTest2" }, + { label: "otherTest" } + ] + }, { + describeBlocks: [], + label: "innerDescribe2", + tests: [ + { label: "someTest" } + ] + }, { + describeBlocks: [], + label: "otherInnerDescribe", + tests: [ + { label: "someTest" } + ] + }], + label: "someDescribe", + tests: [] + }], + file: "/mock-project/some-file.js" + }], + folders: [], + label: "mock-project" + }); + }); + + it('given file id, matches all tests in file', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js` + }) + ]; + + const filteredTree = filterTree(tree, testNames, true); + + expect(filteredTree).toMatchObject({ + files: [{ + describeBlocks: [{ + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [ + { label: "someTest" }, + { label: "someTest2" }, + { label: "otherTest" } + ] + }, { + describeBlocks: [], + label: "innerDescribe2", + tests: [ + { label: "someTest" } + ] + }, { + describeBlocks: [], + label: "otherInnerDescribe", + tests: [ + { label: "someTest" } + ] + }], + label: "someDescribe", + tests: [] + }, { + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [ + { label: "someTest" } + ] + }], + label: "someDescribe2", + tests: [] + }, { + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [ + { label: "someTest" } + ] + }], + label: "otherDescribe", + tests: [] + }], + file: "/mock-project/some-file.js" + }], + folders: [], + label: "mock-project" + }); + }); + + it('given project id, matches all tests in project', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME + }) + ]; + + const filteredTree = filterTree(tree, testNames, true); + + expect(filteredTree).toMatchObject({ + files: [{ + describeBlocks: [{ + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [ + { label: "someTest" }, + { label: "someTest2" }, + { label: "otherTest" } + ] + }, { + describeBlocks: [], + label: "innerDescribe2", + tests: [ + { label: "someTest" } + ] + }, { + describeBlocks: [], + label: "otherInnerDescribe", + tests: [ + { label: "someTest" } + ] + }], + label: "someDescribe", + tests: [] + }, { + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [ + { label: "someTest" } + ] + }], + label: "someDescribe2", + tests: [] + }, { + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [ + { label: "someTest" } + ] + }], + label: "otherDescribe", + tests: [] + }], + file: "/mock-project/some-file.js" + }, { + describeBlocks: [{ + describeBlocks: [{ + describeBlocks: [], + label: "innerDescribe", + tests: [ + { label: "someTest" } + ] + }], + label: "someDescribe", + tests: [] + }], + file: "/mock-project/some-other-file.js" + }], + folders: [], + label: "mock-project" + }); + }); + }); + }); + + describe('jest.each blocks', () => { + const BASE_ID = { projectId: PROJECT_NAME, fileName: ROOT_PATH }; + const tree = createProject(BASE_ID, id => [ + createFile(id, 'some-file.js', id => [ + createDescribe(id, 'someDescribe', 1, id => [ + createTest(id, 'someTest (1)', 2), + createTest(id, 'someTest (2.4)', 3), + createTest(id, 'someTest (-3)', 4), + createTest(id, 'someTest ("a string")', 5), + createTest(id, 'someTest ({"key":"value"})', 6), + createTest(id, 'someTest ([1,2,3])', 7), + createTest(id, 'someTest {"not":"in-parens"}', 8), + createTest(id, 'someTest (random words)', 9), + createTest(id, 'someTest not in parens', 10), + createTest(id, 'someTest 5', 10), + createTest(id, 'someTest (%#)', 11), + createTest(id, 'someTest (%i)', 11), + createTest(id, 'someTest (%d)', 11), + createTest(id, 'someTest (%f)', 11), + createTest(id, 'someTest (%p)', 11), + createTest(id, 'someTest (%s)', 11), + createTest(id, 'someTest (%o)', 11), + createTest(id, 'someTest (%j)', 11) + ]) + ]) + ]); + + const getFiles = (tree: ProjectRootNode|FolderNode): (FileNode|FileWithParseErrorNode)[] => { + return [ + ...tree.files, + ...flatMap(tree.folders, folder => getFiles(folder)) + ]; + }; + + const getTests = (describeBlocks: DescribeNode[]): TestNode[] => { + return [ + ...flatMap(describeBlocks, describe => describe.tests), + ...flatMap(describeBlocks, describe => getTests(describe.describeBlocks)) + ]; + }; + + const getTestLabels = (tree: ProjectRootNode): string[] => { + return flattenDeep(getFiles(tree).map(file => + getTests(file.describeBlocks).map(test => + test.label))); + }; + + it('%#', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe'], + testId: 'someTest (%#)' + }) + ]; + + const filteredTree = filterTree(tree, testNames, true); + + const tests = getTestLabels(filteredTree); + expect(tests).toMatchObject([ + "someTest (1)", + "someTest (%#)", + ]); + }); + + it('%i', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe'], + testId: 'someTest (%i)' + }) + ]; + + const filteredTree = filterTree(tree, testNames, true); + + const tests = getTestLabels(filteredTree); + expect(tests).toMatchObject([ + "someTest (1)", + "someTest (-3)", + "someTest (%i)" + ]); + }); + + ['%d', '%f'].forEach(placeholder => it(`${placeholder}`, () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe'], + testId: `someTest (${placeholder})` + }) + ]; + + const filteredTree = filterTree(tree, testNames, true); + + const tests = getTestLabels(filteredTree); + expect(tests).toMatchObject([ + "someTest (1)", + "someTest (2.4)", + "someTest (-3)", + `someTest (${placeholder})` + ]); + })); + + it('%j', () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe'], + testId: `someTest (%j)` + }) + ]; + + const filteredTree = filterTree(tree, testNames, true); + + const tests = getTestLabels(filteredTree); + expect(tests).toMatchObject([ + 'someTest (1)', + 'someTest (2.4)', + 'someTest (-3)', + 'someTest ("a string")', + 'someTest ({"key":"value"})', + 'someTest ([1,2,3])', + 'someTest (%j)' + ]); + }); + + ['%p', '%s', '%o'].forEach(placeholder => it(`${placeholder}`, () => { + const testNames = [ + mapIdToString({ + projectId: PROJECT_NAME, + fileName: `${ROOT_PATH}/some-file.js`, + describeIds: ['someDescribe'], + testId: `someTest (${placeholder})` + }) + ]; + + const filteredTree = filterTree(tree, testNames, true); + + const tests = getTestLabels(filteredTree); + expect(tests).toMatchObject([ + 'someTest (1)', + 'someTest (2.4)', + 'someTest (-3)', + 'someTest ("a string")', + 'someTest ({"key":"value"})', + 'someTest ([1,2,3])', + 'someTest (random words)', + 'someTest (%#)', + 'someTest (%i)', + 'someTest (%d)', + 'someTest (%f)', + 'someTest (%p)', + 'someTest (%s)', + 'someTest (%o)', + 'someTest (%j)' + ]); + })); + }); + }); +}); \ No newline at end of file diff --git a/src/helpers/__tests__/idMaps.test.ts b/src/helpers/__tests__/idMaps.test.ts index 80e0eba..6e4b14c 100644 --- a/src/helpers/__tests__/idMaps.test.ts +++ b/src/helpers/__tests__/idMaps.test.ts @@ -1,9 +1,13 @@ -import { mapStringToId } from '../idMaps'; +import { mapStringToId, mapIdToString, mapIdToEscapedRegExpId } from '../idMaps'; import { DESCRIBE_ID_SEPARATOR, PROJECT_ID_SEPARATOR, TEST_ID_SEPARATOR } from "../../constants"; +const PS = PROJECT_ID_SEPARATOR; +const DS = DESCRIBE_ID_SEPARATOR; +const TS = TEST_ID_SEPARATOR; + describe('mapStringToId', () => { it('parses project, file, describe, and test when all are present', () => { - const testString = `someProject${PROJECT_ID_SEPARATOR}someFile${DESCRIBE_ID_SEPARATOR}someDescribe${TEST_ID_SEPARATOR}someTest`; + const testString = `someProject${PS}someFile${DS}someDescribe${DS}${TS}someTest${TS}`; const testId = mapStringToId(testString); @@ -16,7 +20,7 @@ describe('mapStringToId', () => { }); it('parses multiple levels of describes', () => { - const testString = `someProject${PROJECT_ID_SEPARATOR}someFile${DESCRIBE_ID_SEPARATOR}someDescribe1${DESCRIBE_ID_SEPARATOR}someDescribe2${DESCRIBE_ID_SEPARATOR}someDescribe3${TEST_ID_SEPARATOR}someTest`; + const testString = `someProject${PS}someFile${DS}someDescribe1${DS}${DS}someDescribe2${DS}${DS}someDescribe3${DS}${TS}someTest${TS}`; const testId = mapStringToId(testString); @@ -29,7 +33,7 @@ describe('mapStringToId', () => { }); it('parses describe when no test is present', () => { - const testString = `someProject${PROJECT_ID_SEPARATOR}someFile${DESCRIBE_ID_SEPARATOR}someDescribe`; + const testString = `someProject${PS}someFile${DS}someDescribe${DS}`; const testId = mapStringToId(testString); @@ -42,7 +46,7 @@ describe('mapStringToId', () => { }); it('parses test when no describe is present', () => { - const testString = `someProject${PROJECT_ID_SEPARATOR}someFile${TEST_ID_SEPARATOR}someTest`; + const testString = `someProject${PS}someFile${TS}someTest${TS}`; const testId = mapStringToId(testString); @@ -55,7 +59,7 @@ describe('mapStringToId', () => { }); it('parses multiple levels of describes when no test is present', () => { - const testString = `someProject${PROJECT_ID_SEPARATOR}someFile${DESCRIBE_ID_SEPARATOR}someDescribe1${DESCRIBE_ID_SEPARATOR}someDescribe2${DESCRIBE_ID_SEPARATOR}someDescribe3`; + const testString = `someProject${PS}someFile${DS}someDescribe1${DS}${DS}someDescribe2${DS}${DS}someDescribe3${DS}`; const testId = mapStringToId(testString); @@ -68,7 +72,7 @@ describe('mapStringToId', () => { }); it('parses project and file when no describe or test are present', () => { - const testString = `someProject${PROJECT_ID_SEPARATOR}someFile`; + const testString = `someProject${PS}someFile`; const testId = mapStringToId(testString); @@ -92,4 +96,87 @@ describe('mapStringToId', () => { testId: undefined }); }); +}); + +describe('mapIdToString', () => { + describe('round-trip tests', () => { + it('results in the same id when mapping to string and back', () => { + const originalId = { + projectId: 'someProject', + fileName: 'someFile', + describeIds: ['outerDescribe', 'innerDescribe'], + testId: 'aTest' + }; + + const roundTrip = mapStringToId(mapIdToString(originalId)); + + expect(roundTrip).toEqual(originalId); + }); + + it('does not lose special characters', () => { + const originalId = { + projectId: 'some.Pr*jec?', + fileName: 'some-file.js', + describeIds: ['[brackets]and(parenthesis)', '^start|end$'], + testId: '+{more}\\' + }; + + const roundTrip = mapStringToId(mapIdToString(originalId)); + + expect(roundTrip).toEqual(originalId); + }); + + it('works when no test', () => { + const originalId = { + projectId: 'someProject', + fileName: 'someFile', + describeIds: ['outerDescribe', 'innerDescribe'] + }; + + const roundTrip = mapStringToId(mapIdToString(originalId)); + + expect(roundTrip).toEqual(originalId); + }); + + it('works when no describes', () => { + const originalId = { + projectId: 'someProject', + fileName: 'someFile' + }; + + const roundTrip = mapStringToId(mapIdToString(originalId)); + + expect(roundTrip).toEqual(originalId); + }); + + it('works when no file', () => { + const originalId = { + projectId: 'someProject' + }; + + const roundTrip = mapStringToId(mapIdToString(originalId)); + + expect(roundTrip).toEqual(originalId); + }); + }); +}); + +describe('mapIdToEscapedRegExpId', () => { + it('escapes regex special characters', () => { + const unescaped = { + projectId: 'some.Pr*jec?', + fileName: 'some-file.js', + describeIds: ['[brackets]and(parenthesis)', '^start|end$'], + testId: '+{more}\\' + }; + + const escaped = mapIdToEscapedRegExpId(unescaped); + + expect(escaped).toEqual({ + projectId: 'some\\.Pr\\*jec\\?', + fileName: 'some-file\\.js', + describeIds: ['\\[brackets\\]and\\(parenthesis\\)', '\\^start\\|end\\$'], + testId: '\\+\\{more\\}\\\\' + }); + }); }); \ No newline at end of file diff --git a/src/helpers/createTree.ts b/src/helpers/createTree.ts index 4637d0b..a064e30 100644 --- a/src/helpers/createTree.ts +++ b/src/helpers/createTree.ts @@ -179,7 +179,7 @@ const mergeDescribeBlocksAndTests = (itBlocks: ItBlock[], describeBlocks: Descri }; const createDescribeNode = (d: Describe, parentId: string, file: string, runtimeDiscovered: boolean): DescribeNode => { - const expectedDescribeBlockId = `${parentId}${DESCRIBE_ID_SEPARATOR}${d.name}`; + const expectedDescribeBlockId = `${parentId}${DESCRIBE_ID_SEPARATOR}${d.name}${DESCRIBE_ID_SEPARATOR}`; return { describeBlocks: d.describeBlocks.map(x => createDescribeNode(x, expectedDescribeBlockId, file,runtimeDiscovered)), file, @@ -193,7 +193,7 @@ const createDescribeNode = (d: Describe, parentId: string, file: string, runtime }; const createTestNode = (t: ItBlock, parentId: string, file: string, runtimeDiscovered: boolean): TestNode => { - const expectedTestId = `${parentId}${TEST_ID_SEPARATOR}${t.name}`; + const expectedTestId = `${parentId}${TEST_ID_SEPARATOR}${t.name}${TEST_ID_SEPARATOR}`; return { file, id: expectedTestId, diff --git a/src/helpers/escapeRegExp.ts b/src/helpers/escapeRegExp.ts index dbd426f..ef41dad 100644 --- a/src/helpers/escapeRegExp.ts +++ b/src/helpers/escapeRegExp.ts @@ -1,3 +1,57 @@ -export default function escapeRegExp(value: string) { + + +const RGX_INDEX = /(0|[1-9]\d*)/.source; + +const RGX_INT = /(0|(-?[1-9]\d*))/.source; +const REG_NUM_NAMES = 'NaN|-?Infinity'; +const RGX_FRAC = /(\.\d+)/.source; +const RGX_EXP = /(e[+-]\d+)/.source; +const RGX_NUM = `-?${RGX_INT}${RGX_FRAC}?${RGX_EXP}?`; + +const RGX_ANYTHING = '.*'; + +// RFC 4627: https://tools.ietf.org/html/rfc4627 +// Loose JSON Regex, should match all valid JSON **produced by converting parameters to JSON** +// - We expect no whitespace outside of strings +// - We only validate that it is made up of legitimate JSON tokens, no brace-matching +// Some non-valid JSON is also matched, but we can accept that +const REGEX_JSON_LOOSE = '(' + [ + // structural characters + /[\[\]{},:]/.source, + // The allowed literal names (plus undefined) + 'true|false|null|undefined', + // Numeric literals + RGX_NUM, + REG_NUM_NAMES, + // Strings + /"([^"]|\\")*"/.source, + // Circular references + /\[Circular\]/.source +].join('|') + ')+'; + +export function escapeRegExp(value: string) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } + +export function replacePrintfPatterns(testId: string): string { + return testId.replace(/%./g, (printfPattern: string) => { + switch (printfPattern[1]) { + case '#': // %# - Index of the test case. + return `(${RGX_INDEX}|${printfPattern})`; + case 'i': // %i - Integer. + return `(${RGX_INT}|${REG_NUM_NAMES}|${printfPattern})`; + case 'd': // %d - Number. + case 'f': // %f - Floating point value. + return `(${RGX_NUM}|${REG_NUM_NAMES}|${printfPattern})`; + case 'j': // %j - JSON. + return `(${REGEX_JSON_LOOSE}|${printfPattern})`; + case 'p': // %p - pretty-format. + case 's': // %s - String. + case 'o': // %o - Object. + return RGX_ANYTHING; + case '%': // %% - single percent sign ('%'). This does not consume an argument. + default: // Leave everything else alone + return printfPattern; + } + }); +} diff --git a/src/helpers/filterTree.ts b/src/helpers/filterTree.ts index 2f1552e..7ae7c35 100644 --- a/src/helpers/filterTree.ts +++ b/src/helpers/filterTree.ts @@ -7,12 +7,14 @@ import { TestNode, WorkspaceRootNode, } from "./tree"; +import { mapTestIdToDescribeIdPattern, mapTestIdToTestIdPattern } from './mapTestIdsToTestFilter' -function filterTree(tree: WorkspaceRootNode, testNames: string[]): WorkspaceRootNode; -function filterTree(tree: ProjectRootNode, testNames: string[]): ProjectRootNode; +function filterTree(tree: WorkspaceRootNode, testNames: string[], useRegex: boolean): WorkspaceRootNode; +function filterTree(tree: ProjectRootNode, testNames: string[], useRegex: boolean): ProjectRootNode; function filterTree( tree: WorkspaceRootNode | ProjectRootNode, testNames: string[], + useRegex: boolean, ): WorkspaceRootNode | ProjectRootNode { if (testNames.length === 0 || testNames[0] === "root") { return tree; @@ -20,21 +22,21 @@ function filterTree( switch (tree.type) { case "workspaceRootNode": - return filterWorkspace(tree as WorkspaceRootNode, testNames); + return filterWorkspace(tree as WorkspaceRootNode, testNames, useRegex); case "projectRootNode": - return filterProject(tree as ProjectRootNode, testNames); + return filterProject(tree as ProjectRootNode, testNames, useRegex); } } -const filterWorkspace = (tree: WorkspaceRootNode, testNames: string[]): WorkspaceRootNode => { +const filterWorkspace = (tree: WorkspaceRootNode, testNames: string[], useRegex: boolean): WorkspaceRootNode => { return { ...tree, - projects: tree.projects.map(p => filterProject(p, testNames)), + projects: tree.projects.map(p => filterProject(p, testNames, useRegex)), }; }; -const filterProject = (project: ProjectRootNode, testNames: string[]): ProjectRootNode => { +const filterProject = (project: ProjectRootNode, testNames: string[], useRegex: boolean): ProjectRootNode => { // if we have been passed a test name that is an exact match for a project, then we should return the whole project. if (testNames.some(t => t === project.id)) { return project; @@ -42,12 +44,12 @@ const filterProject = (project: ProjectRootNode, testNames: string[]): ProjectRo return { ...project, - files: filterFiles(project.files, testNames), - folders: filterFolders(project.folders, testNames), + files: filterFiles(project.files, testNames, useRegex), + folders: filterFolders(project.folders, testNames, useRegex), }; }; -const filterFolders = (folders: FolderNode[], testNames: string[]): FolderNode[] => { +const filterFolders = (folders: FolderNode[], testNames: string[], useRegex: boolean): FolderNode[] => { return folders .filter(folder => testNames.some(testName => testName.startsWith(folder.id))) .map(folder => { @@ -56,8 +58,8 @@ const filterFolders = (folders: FolderNode[], testNames: string[]): FolderNode[] } return { ...folder, - folders: filterFolders(folder.folders, testNames), - files: filterFiles(folder.files, testNames) + folders: filterFolders(folder.folders, testNames, useRegex), + files: filterFiles(folder.files, testNames, useRegex) }; }); }; @@ -65,20 +67,20 @@ const filterFolders = (folders: FolderNode[], testNames: string[]): FolderNode[] const filterFiles = ( files: Array, testNames: string[], + useRegex: boolean, ): Array => { return files .filter(file => testNames.some(testName => testName.startsWith(file.id))) .reduce((acc, file) => { if (testNames.some(testName => testName === file.id)) { acc.push(file); - } - + } else { switch (file.type) { case "file": acc.push({ ...file, - describeBlocks: filterDescribeBlocks(file.describeBlocks, testNames), - tests: filterTests(file.tests, testNames), + describeBlocks: filterDescribeBlocks(file.describeBlocks, testNames, useRegex), + tests: filterTests(file.tests, testNames, useRegex), }); break; @@ -89,28 +91,43 @@ const filterFiles = ( acc.push(file); break; } + } return acc; }, [] as Array); }; -const filterDescribeBlocks = (describeBlocks: DescribeNode[], testNames: string[]): DescribeNode[] => { - return describeBlocks - .filter(describe => testNames.some(testName => testName.startsWith(describe.id))) +const filterDescribeBlocks = (describeBlocks: DescribeNode[], testNames: string[], useRegex: boolean): DescribeNode[] => { + let filteredDescribeBlocks; + if (useRegex) { + const testNamePatterns = testNames.map(testName => new RegExp(mapTestIdToDescribeIdPattern(testName))); + filteredDescribeBlocks = describeBlocks + .filter(describe => testNamePatterns.some(pattern => pattern.test(describe.id))); + } else { + filteredDescribeBlocks = describeBlocks + .filter(describe => testNames.some(testName => testName.startsWith(describe.id))); + } + + return filteredDescribeBlocks .map(describe => { if (testNames.some(testName => testName === describe.id)) { return describe; } return { ...describe, - describeBlocks: filterDescribeBlocks(describe.describeBlocks, testNames), - tests: filterTests(describe.tests, testNames) + describeBlocks: filterDescribeBlocks(describe.describeBlocks, testNames, useRegex), + tests: filterTests(describe.tests, testNames, useRegex) }; }); }; -const filterTests = (tests: TestNode[], testNames: string[]): TestNode[] => { - return tests.filter(test => testNames.some(testName => testName.startsWith(test.id))); -} +const filterTests = (tests: TestNode[], testNames: string[], useRegex: boolean): TestNode[] => { + if (useRegex) { + const testNamePatterns = testNames.map(testName => new RegExp(mapTestIdToTestIdPattern(testName))); + return tests.filter(test => testNamePatterns.some(pattern => pattern.test(test.id))); + } else { + return tests.filter(test => testNames.some(testName => testName.startsWith(test.id))); + } +}; export { filterTree }; diff --git a/src/helpers/idMaps.ts b/src/helpers/idMaps.ts index fabe10c..ae447d2 100644 --- a/src/helpers/idMaps.ts +++ b/src/helpers/idMaps.ts @@ -1,4 +1,5 @@ import { DESCRIBE_ID_SEPARATOR, PROJECT_ID_SEPARATOR, TEST_ID_SEPARATOR } from "../constants"; +import { escapeRegExp } from "./escapeRegExp"; interface Id { projectId: string; @@ -14,11 +15,11 @@ const mapIdToString = (id: Id): string => { result += `${PROJECT_ID_SEPARATOR}${id.fileName}`; if (id.describeIds && id.describeIds.length > 0) { - result += `${DESCRIBE_ID_SEPARATOR}${id.describeIds.join(DESCRIBE_ID_SEPARATOR)}`; + result += `${DESCRIBE_ID_SEPARATOR}${id.describeIds.join(`${DESCRIBE_ID_SEPARATOR}${DESCRIBE_ID_SEPARATOR}`)}${DESCRIBE_ID_SEPARATOR}`; } if (id.testId) { - result += `${TEST_ID_SEPARATOR}${id.testId}`; + result += `${TEST_ID_SEPARATOR}${id.testId}${TEST_ID_SEPARATOR}`; } } @@ -30,18 +31,43 @@ const mapStringToId = (id: string): Id => { RegExp(`(?[^${PROJECT_ID_SEPARATOR}]*)(${PROJECT_ID_SEPARATOR}(?[^${DESCRIBE_ID_SEPARATOR}${TEST_ID_SEPARATOR}]*)?(?.*))?`), )?.groups || {}; - // TestID is everything after first TEST_ID_SEPARATOR, if we find multiple TEST_ID_SEPARATORs, add them back in - const [ describes, ...testIdParts ] = (rest || '').split(TEST_ID_SEPARATOR); + // TestID is everything after first TEST_ID_SEPARATOR and ends with TEST_ID_SEPARATORs + // if we find multiple TEST_ID_SEPARATORs in the middle, add them back in + const [ describes, ...testIdParts ] = (rest || '') + .replace(new RegExp(`${TEST_ID_SEPARATOR}$`), '') // Remove trailing TEST_ID_SEPARATOR + .split(TEST_ID_SEPARATOR); const testId = testIdParts.join(TEST_ID_SEPARATOR) || undefined; - // Remaining string will start with DESCRIBE_ID_SEPARATOR, so throw away first part when splitting - const [, ...describeIds] = describes.split(DESCRIBE_ID_SEPARATOR); + // describeIDs are wrapped with DESCRIBE_ID_SEPARATOR + const describeIds = !describes.length + ? [] + : describes + .replace(new RegExp(`^${DESCRIBE_ID_SEPARATOR}(.*)${DESCRIBE_ID_SEPARATOR}$`),'$1') + .split(`${DESCRIBE_ID_SEPARATOR}${DESCRIBE_ID_SEPARATOR}`); return { - describeIds: describeIds.length ? describeIds : undefined, - fileName, projectId, + fileName, + describeIds: describeIds.length ? describeIds : undefined, testId }; }; -export { mapIdToString, mapStringToId, Id }; +const mapIdToEscapedRegExpId = (id: Id): Id => { + return { + projectId: escapeRegExp(id.projectId), + + fileName: (id.fileName === undefined + ? undefined + : escapeRegExp(id.fileName)), + + describeIds: (id.describeIds === undefined + ? undefined + : id.describeIds.map(escapeRegExp)), + + testId: (id.testId === undefined + ? undefined + : escapeRegExp(id.testId)) + }; +} + +export { mapIdToString, mapStringToId, mapIdToEscapedRegExpId, Id }; diff --git a/src/helpers/mapTestIdsToTestFilter.ts b/src/helpers/mapTestIdsToTestFilter.ts index 64ce965..320db6a 100644 --- a/src/helpers/mapTestIdsToTestFilter.ts +++ b/src/helpers/mapTestIdsToTestFilter.ts @@ -1,14 +1,78 @@ import _ from 'lodash'; +import { DESCRIBE_ID_SEPARATOR, TEST_ID_SEPARATOR } from '../constants'; import { ITestFilter } from "../types"; -import escapeRegExp from "./escapeRegExp"; -import { mapStringToId, Id } from "./idMaps"; +import { replacePrintfPatterns } from "./escapeRegExp"; +import { mapStringToId, mapIdToEscapedRegExpId, mapIdToString, Id } from "./idMaps"; -function mapTestIdsToTestNamePattern(test: Id): string { +function isDefined(object: T | undefined): object is T { + return object !== undefined; +}; + +function exactMatchRegex(regexString: string): string { + return `^${regexString}$`; +} + +function startingMatchRegex(regexString: string): string { + return `^${regexString}`; +} + +function mapTestIdToTestNamePattern(test: Id): string { // Jest test names are a concatenation of the describeIds and testId, separated by space - return (test.describeIds || []).concat(test.testId || '') - .filter(testPart => testPart) - .map(part => escapeRegExp(part || "")) + const describeIds = (test.describeIds || []); + const testId = test.testId === undefined + // If there's NO testId, add empty space to at least require SOMETHING after the last describe (helps prevent partial matches on describes) + ? '' + // If there IS a testId, require exact match + : test.testId + '$'; + const regex = [...describeIds, testId] + .filter(isDefined) + .map(replacePrintfPatterns) .join(' '); + return startingMatchRegex(regex); +} + +export function mapTestIdToDescribeIdPattern(test: string): string { + return [test] + .map(mapStringToId) + .map(mapIdToEscapedRegExpId) + .map(testId => ({ + projectId: testId.projectId, + fileName: testId.fileName, + describeIds: testId.describeIds?.map(replacePrintfPatterns), + testId: testId.testId === undefined ? undefined : replacePrintfPatterns(testId.testId) + })) + .map(mapIdToString) + .map(regexString => { + const parts = regexString.split(`${DESCRIBE_ID_SEPARATOR}${DESCRIBE_ID_SEPARATOR}`); + return parts.join(`${DESCRIBE_ID_SEPARATOR}(${DESCRIBE_ID_SEPARATOR}`) + + (')?'.repeat(parts.length - 1)); + }) + .map(regexString => { + const parts = regexString.split(`${DESCRIBE_ID_SEPARATOR}${TEST_ID_SEPARATOR}`); + return parts.join(`${DESCRIBE_ID_SEPARATOR}(${TEST_ID_SEPARATOR}`) + + (')?'.repeat(parts.length - 1)) + }) + .map(exactMatchRegex) + [0]; +} + +export function mapTestIdToTestIdPattern(test: string): string { + return [test] + .map(mapStringToId) + .map(mapIdToEscapedRegExpId) + .map(testId => ({ + projectId: testId.projectId, + fileName: testId.fileName, + describeIds: testId.describeIds?.map(replacePrintfPatterns), + testId: testId.testId === undefined ? undefined : replacePrintfPatterns(testId.testId) + })) + .map(testId => { + const regex = mapIdToString(testId); + return (testId.testId + ? exactMatchRegex(regex) + : startingMatchRegex(regex)); + }) + [0]; } export function mapTestIdsToTestFilter(tests: string[]): ITestFilter | null { @@ -16,7 +80,7 @@ export function mapTestIdsToTestFilter(tests: string[]): ITestFilter | null { return null; } - const ids = tests.map(t => mapStringToId(t)); + const ids = tests.map(mapStringToId).map(mapIdToEscapedRegExpId); // if there are any ids that do not contain a fileName, then we should run all the tests in the project. if (_.some(ids, x => !x.fileName)) { @@ -25,10 +89,10 @@ export function mapTestIdsToTestFilter(tests: string[]): ITestFilter | null { // we accumulate the file and test names into regex expressions. Note we escape the names to avoid interpreting // any regex control characters in the file or test names. - const testNamePattern = ids.map(mapTestIdsToTestNamePattern) + const testNamePattern = ids.map(id => mapTestIdToTestNamePattern(id)) .filter(testId => testId) .join("|"); - const testFileNamePattern = ids.filter(x => x.fileName).map(z => escapeRegExp(z.fileName || "")).join("|"); + const testFileNamePattern = ids.filter(x => x.fileName).map(z => z.fileName || "").join("|"); return { testFileNamePattern, diff --git a/src/helpers/mergeRuntimeResults.ts b/src/helpers/mergeRuntimeResults.ts index 5f68e90..5d0a750 100644 --- a/src/helpers/mergeRuntimeResults.ts +++ b/src/helpers/mergeRuntimeResults.ts @@ -15,10 +15,10 @@ import { const mergeRuntimeResults = (tree: ProjectRootNode, testResults: JestFileResults[]): ProjectRootNode => { const filesUpdate = (files: Array) => { - return files.map(f => { - const result = testResults.filter(x => lowerCaseDriveLetter(x.name) === f.file)[0]; + return files.map(file => { + const result = testResults.filter(x => lowerCaseDriveLetter(x.name) === file.file)[0]; if (!result) { - return f; + return file; } const processDescribes = ( @@ -33,9 +33,9 @@ const mergeRuntimeResults = (tree: ProjectRootNode, testResults: JestFileResults if (match) { return describeBlocks.map(x => (x === match ? processFileOrDescribe(x, others, assertion) : x)); } else { - const id = `${parentId}${DESCRIBE_ID_SEPARATOR}${describeName}`; + const id = `${parentId}${DESCRIBE_ID_SEPARATOR}${describeName}${DESCRIBE_ID_SEPARATOR}`; return describeBlocks.concat( - processFileOrDescribe(createDescribeNode(id, describeName, f.file, undefined, true), others, assertion), + processFileOrDescribe(createDescribeNode(id, describeName, file.file, undefined, true), others, assertion), ); } } @@ -66,8 +66,8 @@ const mergeRuntimeResults = (tree: ProjectRootNode, testResults: JestFileResults const processTests = (parentId: string, tests: TestNode[], assertion: JestAssertionResults): TestNode[] => { if (!_.some(tests, t => t.label === assertion.title)) { - const id = `${parentId}${TEST_ID_SEPARATOR}${assertion.title}`; - const newTest = createTestNode(id, assertion.title, f.file, undefined, true); + const id = `${parentId}${TEST_ID_SEPARATOR}${assertion.title}${TEST_ID_SEPARATOR}`; + const newTest = createTestNode(id, assertion.title, file.file, undefined, true); return tests.concat(newTest); } return tests; @@ -75,7 +75,7 @@ const mergeRuntimeResults = (tree: ProjectRootNode, testResults: JestFileResults return result.assertionResults.reduce( (acc, current) => processFileOrDescribe(acc, current.ancestorTitles ?? [], current), - f, + file, ); }); };