From 62184661f362e6c4e2707342c54091295590b840 Mon Sep 17 00:00:00 2001 From: Dom Slee Date: Sun, 15 Oct 2023 16:44:34 +1100 Subject: [PATCH] fix: Shell escaping test coverage and edge cases --- examples/examples.test.ts | 8 + package-lock.json | 552 +++++++++++++++++++++++++++++- package.json | 3 +- src/JestRunnerCodeLensProvider.ts | 11 +- src/codeLensUtil.ts | 29 ++ src/commandBuilder.ts | 57 +++ src/jestRunner.ts | 63 +--- src/jestRunnerConfig.ts | 11 +- src/test/commandBuilder.test.ts | 68 ++++ src/test/util/runJestCommand.ts | 69 ++++ src/test/util/shellHandler.ts | 34 ++ src/test/utils.test.ts | 2 +- src/util.ts | 44 +-- 13 files changed, 850 insertions(+), 101 deletions(-) create mode 100644 src/codeLensUtil.ts create mode 100644 src/commandBuilder.ts create mode 100644 src/test/commandBuilder.test.ts create mode 100644 src/test/util/runJestCommand.ts create mode 100644 src/test/util/shellHandler.ts diff --git a/examples/examples.test.ts b/examples/examples.test.ts index f69a54d..8004048 100644 --- a/examples/examples.test.ts +++ b/examples/examples.test.ts @@ -19,6 +19,7 @@ describe('Example tests', () => { expect(true); }); + // #319 it(`test with lf`, () => { expect(true); @@ -54,6 +55,13 @@ lf`, () => { }); }); +// #295 +describe('a "(name)"', () => { + it('mix of paranthesis and double quotes', () => { + expect(1).toBe(1); + }); +}); + // #311 it.each([1, 2])('test with generated %i', (id) => { expect(true); diff --git a/package-lock.json b/package-lock.json index aa3db21..5ba3999 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "jest": "^29.7.0", "ovsx": "^0.8.3", "prettier": "^3.0.3", + "rimraf": "^5.0.5", "ts-jest": "^29.1.1", "ts-loader": "^9.5.0", "typescript": "^5.2.2", @@ -781,6 +782,102 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1275,6 +1372,16 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgr/utils": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", @@ -3033,6 +3140,12 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "node_modules/electron-to-chromium": { "version": "1.4.284", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", @@ -3577,6 +3690,21 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/flatted": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", @@ -3603,6 +3731,34 @@ } } }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -4224,6 +4380,24 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -5449,6 +5623,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -5777,6 +5960,31 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", + "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -6214,15 +6422,64 @@ } }, "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", + "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", "dev": true, "dependencies": { - "glob": "^7.1.3" + "glob": "^10.3.7" }, "bin": { - "rimraf": "bin.js" + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -6525,6 +6782,21 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -6537,6 +6809,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -6785,6 +7070,21 @@ "node": ">=8.17.0" } }, + "node_modules/tmp/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -7255,6 +7555,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -7906,6 +8224,71 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -8297,6 +8680,13 @@ "fastq": "^1.6.0" } }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, "@pkgr/utils": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", @@ -9583,6 +9973,12 @@ "domhandler": "^5.0.1" } }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "electron-to-chromium": { "version": "1.4.284", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", @@ -9979,6 +10375,17 @@ "requires": { "flatted": "^3.1.0", "rimraf": "^3.0.2" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "flatted": { @@ -9993,6 +10400,24 @@ "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", "dev": true }, + "foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + } + } + }, "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -10427,6 +10852,16 @@ "istanbul-lib-report": "^3.0.0" } }, + "jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, "jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -11379,6 +11814,12 @@ "dev": true, "optional": true }, + "minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true + }, "mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -11631,6 +12072,24 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "requires": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", + "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "dev": true + } + } + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -11940,12 +12399,45 @@ "dev": true }, "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", + "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", "dev": true, "requires": { - "glob": "^7.1.3" + "glob": "^10.3.7" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + } + }, + "minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } } }, "run-applescript": { @@ -12142,6 +12634,17 @@ "strip-ansi": "^6.0.1" } }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -12151,6 +12654,15 @@ "ansi-regex": "^5.0.1" } }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, "strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -12320,6 +12832,17 @@ "dev": true, "requires": { "rimraf": "^3.0.0" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "tmpl": { @@ -12638,6 +13161,17 @@ "strip-ansi": "^6.0.0" } }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 60102b4..a633e89 100644 --- a/package.json +++ b/package.json @@ -211,16 +211,17 @@ "@types/vscode": "^1.83.0", "@typescript-eslint/eslint-plugin": "^6.7.5", "@typescript-eslint/parser": "^6.7.5", + "@vscode/vsce": "^2.21.1", "eslint": "^8.51.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.1", "jest": "^29.7.0", "ovsx": "^0.8.3", "prettier": "^3.0.3", + "rimraf": "^5.0.5", "ts-jest": "^29.1.1", "ts-loader": "^9.5.0", "typescript": "^5.2.2", - "@vscode/vsce": "^2.21.1", "webpack": "^5.89.0", "webpack-cli": "^5.1.4" }, diff --git a/src/JestRunnerCodeLensProvider.ts b/src/JestRunnerCodeLensProvider.ts index 5ba1ea3..f142953 100644 --- a/src/JestRunnerCodeLensProvider.ts +++ b/src/JestRunnerCodeLensProvider.ts @@ -1,6 +1,6 @@ import { parse, ParsedNode } from './parser'; import { CodeLens, CodeLensProvider, Range, TextDocument } from 'vscode'; -import { findFullTestName, escapeRegExp, CodeLensOption } from './util'; +import { findFullTestName, CodeLensOption } from './codeLensUtil'; function getCodeLensForOption(range: Range, codeLensOption: CodeLensOption, fullTestName: string): CodeLens { const titleMap: Record = { @@ -44,7 +44,7 @@ function getTestsBlocks( return []; } - const fullTestName = escapeRegExp(findFullTestName(parsedNode.start.line, parseResults)); + const fullTestName = findFullTestName(parsedNode.start.line, parseResults); codeLens.push(...codeLensOptions.map((option) => getCodeLensForOption(range, option, fullTestName))); @@ -55,9 +55,12 @@ export class JestRunnerCodeLensProvider implements CodeLensProvider { constructor(private readonly codeLensOptions: CodeLensOption[]) {} public async provideCodeLenses(document: TextDocument): Promise { + return this.getCodeLenses(document.fileName, document.getText()); + } + + public async getCodeLenses(documentFileName: string, documentText: string): Promise { try { - const text = document.getText(); - const parseResults = parse(document.fileName, text, { plugins: { decorators: 'legacy' } }).root.children; + const parseResults = parse(documentFileName, documentText, { plugins: { decorators: 'legacy' } }).root.children; const codeLens: CodeLens[] = []; parseResults.forEach((parseResult) => codeLens.push(...getTestsBlocks(parseResult, parseResults, this.codeLensOptions)) diff --git a/src/codeLensUtil.ts b/src/codeLensUtil.ts new file mode 100644 index 0000000..e34e738 --- /dev/null +++ b/src/codeLensUtil.ts @@ -0,0 +1,29 @@ +export function findFullTestName(selectedLine: number, children: any[]): string | undefined { + if (!children) { + return; + } + for (const element of children) { + if (element.type === 'describe' && selectedLine === element.start.line) { + return element.name; + } + if (element.type !== 'describe' && selectedLine >= element.start.line && selectedLine <= element.end.line) { + return element.name; + } + } + for (const element of children) { + const result = findFullTestName(selectedLine, element.children); + if (result) { + return element.name + ' ' + result; + } + } +} + +export type CodeLensOption = 'run' | 'debug' | 'watch' | 'coverage'; + +function isCodeLensOption(option: string): option is CodeLensOption { + return ['run', 'debug', 'watch', 'coverage'].includes(option); +} + +export function validateCodeLensOptions(maybeCodeLensOptions: string[]): CodeLensOption[] { + return [...new Set(maybeCodeLensOptions)].filter((value) => isCodeLensOption(value)) as CodeLensOption[]; +} diff --git a/src/commandBuilder.ts b/src/commandBuilder.ts new file mode 100644 index 0000000..e624523 --- /dev/null +++ b/src/commandBuilder.ts @@ -0,0 +1,57 @@ +import { IJestRunnerCommandBuilderConfig } from './jestRunnerConfig'; +import { + escapeRegExp, + escapeRegExpForPath, + escapeQuotesInTestName, + normalizePath, + quote, + resolveTestNameStringInterpolation, + updateTestNameIfUsingProperties, +} from './util'; + +export class CommandBuilder { + constructor(private readonly config: IJestRunnerCommandBuilderConfig) {} + + buildJestCommand(filePath: string, testName?: string, options?: string[]): string { + const args = this.buildJestArgs(filePath, testName, options); + return `${this.config.jestCommand} ${args.join(' ')}`; + } + + buildJestArgs(filePath: string, testName: string | undefined, options: string[] = []): string[] { + const args: string[] = []; + + args.push(quote(escapeRegExpForPath(normalizePath(filePath)))); + + const jestConfigPath = this.config.getJestConfigPath(filePath); + if (jestConfigPath) { + args.push('-c'); + args.push(quote(normalizePath(jestConfigPath))); + } + + if (testName) { + args.push('-t'); + testName = resolveTestName(testName); + args.push(quote(testName)); + } + + const setOptions = new Set(options); + + if (this.config.runOptions) { + this.config.runOptions.forEach((option) => setOptions.add(option)); + } + + args.push(...setOptions); + + return args; + } +} + +function resolveTestName(testName: string): string { + testName = updateTestNameIfUsingProperties(testName); + testName = resolveTestNameStringInterpolation(testName); + testName = escapeRegExp(testName); + testName = escapeQuotesInTestName(testName); + testName = testName.replace(/\n/g, '\\n'); + testName = testName.replace(/\r/g, '\\r'); + return testName; +} diff --git a/src/jestRunner.ts b/src/jestRunner.ts index 3597673..da721a3 100644 --- a/src/jestRunner.ts +++ b/src/jestRunner.ts @@ -2,17 +2,9 @@ import * as vscode from 'vscode'; import { JestRunnerConfig } from './jestRunnerConfig'; import { parse } from './parser'; -import { - escapeRegExp, - escapeRegExpForPath, - escapeSingleQuotes, - findFullTestName, - normalizePath, - pushMany, - quote, - unquote, - updateTestNameIfUsingProperties, -} from './util'; +import { pushMany, quote, unquote } from './util'; +import { CommandBuilder } from './commandBuilder'; +import { findFullTestName } from './codeLensUtil'; interface DebugCommand { documentUri: vscode.Uri; @@ -29,9 +21,11 @@ export class JestRunner { // terminal after all commands been pushed private openNativeTerminal: boolean; private commands: string[] = []; + private readonly commandBuilder: CommandBuilder; constructor(private readonly config: JestRunnerConfig) { this.setup(); + this.commandBuilder = new CommandBuilder(this.config); this.openNativeTerminal = config.isRunInExternalNativeTerminal; } @@ -40,7 +34,7 @@ export class JestRunner { // public async runTestsOnPath(path: string): Promise { - const command = this.buildJestCommand(path); + const command = this.commandBuilder.buildJestCommand(path); this.previousCommand = command; @@ -61,8 +55,7 @@ export class JestRunner { const filePath = editor.document.fileName; const testName = currentTestName || this.findCurrentTestName(editor); - const resolvedTestName = updateTestNameIfUsingProperties(testName); - const command = this.buildJestCommand(filePath, resolvedTestName, options); + const command = this.commandBuilder.buildJestCommand(filePath, testName, options); this.previousCommand = command; @@ -81,7 +74,7 @@ export class JestRunner { await editor.document.save(); const filePath = editor.document.fileName; - const command = this.buildJestCommand(filePath, undefined, options); + const command = this.commandBuilder.buildJestCommand(filePath, undefined, options); this.previousCommand = command; @@ -131,8 +124,7 @@ export class JestRunner { const filePath = editor.document.fileName; const testName = currentTestName || this.findCurrentTestName(editor); - const resolvedTestName = updateTestNameIfUsingProperties(testName); - const debugConfig = this.getDebugConfig(filePath, resolvedTestName); + const debugConfig = this.getDebugConfig(filePath, testName); await this.goToCwd(); await this.executeDebugCommand({ @@ -180,7 +172,7 @@ export class JestRunner { config.program = `.yarn/releases/${this.config.getYarnPnpCommand}`; } - const standardArgs = this.buildJestArgs(filePath, currentTestName, false); + const standardArgs = this.commandBuilder.buildJestArgs(filePath, currentTestName); pushMany(config.args, standardArgs); config.args.push('--runInBand'); @@ -199,40 +191,7 @@ export class JestRunner { const testFile = parse(filePath); const fullTestName = findFullTestName(selectedLine, testFile.root.children); - return fullTestName ? escapeRegExp(fullTestName) : undefined; - } - - private buildJestCommand(filePath: string, testName?: string, options?: string[]): string { - const args = this.buildJestArgs(filePath, testName, true, options); - return `${this.config.jestCommand} ${args.join(' ')}`; - } - - private buildJestArgs(filePath: string, testName: string, withQuotes: boolean, options: string[] = []): string[] { - const args: string[] = []; - const quoter = withQuotes ? quote : (str) => str; - - args.push(quoter(escapeRegExpForPath(normalizePath(filePath)))); - - const jestConfigPath = this.config.getJestConfigPath(filePath); - if (jestConfigPath) { - args.push('-c'); - args.push(quoter(normalizePath(jestConfigPath))); - } - - if (testName) { - args.push('-t'); - args.push(quoter(escapeSingleQuotes(testName))); - } - - const setOptions = new Set(options); - - if (this.config.runOptions) { - this.config.runOptions.forEach((option) => setOptions.add(option)); - } - - args.push(...setOptions); - - return args; + return fullTestName; } private async goToCwd() { diff --git a/src/jestRunnerConfig.ts b/src/jestRunnerConfig.ts index 9129599..5c430b8 100644 --- a/src/jestRunnerConfig.ts +++ b/src/jestRunnerConfig.ts @@ -1,9 +1,10 @@ import * as path from 'path'; import * as fs from 'fs'; import * as vscode from 'vscode'; -import { normalizePath, quote, validateCodeLensOptions, CodeLensOption, isNodeExecuteAbleFile } from './util'; +import { normalizePath, quote, isNodeExecuteAbleFile } from './util'; +import { CodeLensOption, validateCodeLensOptions } from './codeLensUtil'; -export class JestRunnerConfig { +export class JestRunnerConfig implements IJestRunnerCommandBuilderConfig { /** * The command that runs jest. * Defaults to: node "node_modules/.bin/jest" @@ -161,3 +162,9 @@ export class JestRunnerConfig { return yarnPnpCommand; } } + +export interface IJestRunnerCommandBuilderConfig { + jestCommand: string; + getJestConfigPath(filePath: string): string; + runOptions?: Array; +} diff --git a/src/test/commandBuilder.test.ts b/src/test/commandBuilder.test.ts new file mode 100644 index 0000000..414d9e1 --- /dev/null +++ b/src/test/commandBuilder.test.ts @@ -0,0 +1,68 @@ +import { tmpdir } from 'os'; +import { mkdtempSync } from 'fs'; +import { rimrafSync } from 'rimraf'; +import * as path from 'path'; +import { runJestCommand } from './util/runJestCommand'; +import { Shell } from './util/shellHandler'; +import { isWindows } from '../util'; + +const ALL_SHELLS: Array = isWindows() ? ['cmd'] : ['bash']; + +describe('CommandBuilder', () => { + let tempDir: string; + + beforeEach(() => { + tempDir = mkdtempSync(path.resolve(tmpdir(), 'commandBuilder')); + }); + + afterEach(() => { + rimrafSync(tempDir); + }); + + describe.each(ALL_SHELLS)('mustMatchSelf (%s)', (shell: Shell) => { + it('single quote', () => mustMatchSelf(shell, `test with ' single quote`)); + it('double quote', () => mustMatchSelf(shell, 'test with " double quote')); + it('parenthesis', () => mustMatchSelf(shell, 'test with () parenthesis')); + it('lf', () => mustMatchSelf(shell, `test with \nlf`)); + it('lf#2', () => mustMatchSelf(shell, 'test with \nmanual lf')); + it('crlf', () => mustMatchSelf(shell, 'test with \r\nmanual crlf')); + it('backticks', () => mustMatchSelf(shell, 'test with `backticks`')); + it('regex', () => mustMatchSelf(shell, 'test with regex .*$^|[]')); + it('prototype .name property', () => mustMatchSelf(shell, 'TestClass.prototype.myFunction.name', 'myFunction')); + it('mix of quotes and paranthesis', () => + mustMatchSelf(shell, 'a "(name)"', 'mix of paranthesis and double quotes')); + }); + + async function mustMatchSelf(shell: Shell, testName: string, expectedTestName?: string) { + if (shouldSkipMustMatchSelf(shell, testName)) { + return; + } + const jestJson = await runJestCommand(shell, tempDir, testName); + const expectedPassedTests = [expectedTestName ?? testName]; + return expect(jestJson).toEqual( + expect.objectContaining({ + passedTests: expectedPassedTests, + }) + ); + } + + // FIXME: these are broken + function shouldSkipMustMatchSelf(_shell: Shell, _testName: string): boolean { + return false; + } + + describe.each(ALL_SHELLS)('mustMatchAll (%s)', (shell: Shell) => { + it('all match', () => mustMatchAll(shell, 'test with ')); + it('using %', () => mustMatchAll(shell, 'test with %p')); + it('using $', () => mustMatchAll(shell, 'test with $var')); + }); + + async function mustMatchAll(shell: Shell, testName: string) { + const jestJson = await runJestCommand(shell, tempDir, testName); + expect(jestJson).toEqual( + expect.objectContaining({ + numPassedTests: 15, + }) + ); + } +}); diff --git a/src/test/util/runJestCommand.ts b/src/test/util/runJestCommand.ts new file mode 100644 index 0000000..0ad7177 --- /dev/null +++ b/src/test/util/runJestCommand.ts @@ -0,0 +1,69 @@ +import * as path from 'path'; +import { CommandBuilder } from '../../commandBuilder'; +import { IJestRunnerCommandBuilderConfig } from '../../jestRunnerConfig'; +import { spawnSync } from 'child_process'; +import { writeFileSync } from 'fs'; +import { Shell, getArgsForShell, getCommandPrefix, getFileExtension } from './shellHandler'; + +const jestRunnerConfig: IJestRunnerCommandBuilderConfig = { + jestCommand: 'node "node_modules/jest/bin/jest.js"', + getJestConfigPath: function (filePath: string): string { + return ''; + }, +}; +const testFileName = path.resolve(__dirname, '..', '..', '..', 'examples', 'examples.test.ts'); +const packageJsonDirectory = getPackageJsonDirectory(); + +export async function runJestCommand(shell: Shell, tempDir: string, testName: string) { + const commandBuilder = new CommandBuilder(jestRunnerConfig); + const command = commandBuilder.buildJestCommand(testFileName, testName, ['--json']); + const testFilePath = path.resolve(tempDir, 'mytest' + getFileExtension(shell)); + writeFileSync(testFilePath, getCommandPrefix(shell) + command); + const result = spawnSync(shell, getArgsForShell(shell, testFilePath), { cwd: packageJsonDirectory }); + + const stdOutString = result.stdout.toString(); + const stdErrString = result.stderr.toString(); + + if (result.status !== 0) { + throw new Error( + `Command failed (${result.status}). command: ${command}. + STDERR: + ${stdErrString} + + STDOUT: + ${stdOutString}` + ); + } + + return parseJsonFromStdout(command, stdOutString); +} + +function parseJsonFromStdout(command: string, stdOutString: string) { + let outputJSON; + try { + outputJSON = JSON.parse(stdOutString); + } catch (e) { + throw new Error(`Failed to parse output: ${stdOutString}. Command: \`${command}\`\n. Error: ${e}`); + } + + const passedTests = new Array(); + for (const testResult of outputJSON.testResults) { + for (const assertionResult of testResult.assertionResults) { + if (assertionResult.status === 'passed') { + passedTests.push(assertionResult.title); + } + } + } + + return { + numPassedTests: outputJSON.numPassedTests, + numFailedTests: outputJSON.numFailedTests, + numPendingTests: outputJSON.numPendingTests, + passedTests, + }; +} + +function getPackageJsonDirectory() { + const packageJsonDir = path.dirname(require.resolve('../../../package.json')); + return packageJsonDir; +} diff --git a/src/test/util/shellHandler.ts b/src/test/util/shellHandler.ts new file mode 100644 index 0000000..76ed4d3 --- /dev/null +++ b/src/test/util/shellHandler.ts @@ -0,0 +1,34 @@ +export type Shell = 'bash' | 'pwsh' | 'powershell' | 'cmd'; + +export function getArgsForShell(shell: Shell, testFilePath: string): Array { + if (['pwsh', 'powershell'].includes(shell)) { + return ['-NoProfile', '-File', testFilePath]; + } + if (shell === 'bash') { + return ['--noprofile', testFilePath]; + } + if (shell === 'cmd') { + return ['/c', testFilePath]; + } + throw new Error('unhandled shell'); +} + +export function getFileExtension(shell: Shell): string { + if (['pwsh', 'powershell'].includes(shell)) { + return '.ps1'; + } + if (shell === 'bash') { + return '.sh'; + } + if (shell === 'cmd') { + return '.bat'; + } + throw new Error('unhandled shell'); +} + +export function getCommandPrefix(shell: Shell): string { + if (shell === 'cmd') { + return '@echo off\n'; + } + return ''; +} diff --git a/src/test/utils.test.ts b/src/test/utils.test.ts index 73d1b73..7ee42f6 100644 --- a/src/test/utils.test.ts +++ b/src/test/utils.test.ts @@ -1,4 +1,4 @@ -import { validateCodeLensOptions } from '../util'; +import { validateCodeLensOptions } from '../codeLensUtil'; describe('validateCodeLensOptions', () => it.each([ diff --git a/src/util.ts b/src/util.ts index c35cead..19fae54 100644 --- a/src/util.ts +++ b/src/util.ts @@ -17,40 +17,23 @@ export function escapeRegExpForPath(s: string): string { return s.replace(/[*+?^${}<>()|[\]]/g, '\\$&'); // $& means the whole matched string } -export function findFullTestName(selectedLine: number, children: any[]): string | undefined { - if (!children) { - return; - } - for (const element of children) { - if (element.type === 'describe' && selectedLine === element.start.line) { - return resolveTestNameStringInterpolation(element.name); - } - if (element.type !== 'describe' && selectedLine >= element.start.line && selectedLine <= element.end.line) { - return resolveTestNameStringInterpolation(element.name); - } - } - for (const element of children) { - const result = findFullTestName(selectedLine, element.children); - if (result) { - return resolveTestNameStringInterpolation(element.name) + ' ' + result; - } - } -} - const QUOTES = { '"': true, "'": true, '`': true, }; -function resolveTestNameStringInterpolation(s: string): string { +export function resolveTestNameStringInterpolation(s: string): string { const variableRegex = /(\${?[A-Za-z0-9_]+}?|%[psdifjo#%])/gi; const matchAny = '(.*?)'; return s.replace(variableRegex, matchAny); } -export function escapeSingleQuotes(s: string): string { - return isWindows() ? s : s.replace(/'/g, "'\\''"); +export function escapeQuotesInTestName(s: string): string { + if (isWindows()) { + return s.replace(/"/g, '\\"'); + } + return s.replace(/'/g, "'\\''"); } export function quote(s: string): string { @@ -93,14 +76,11 @@ export function isNodeExecuteAbleFile(filepath: string): boolean { } } -export function updateTestNameIfUsingProperties(receivedTestName?: string) { - if (receivedTestName === undefined) { - return undefined; +export function updateTestNameIfUsingProperties(receivedTestName: string): string { + const prototypePropertyRegex = /^(?:\w*\.prototype)?\.(\w*).name$/; + const match = receivedTestName.match(prototypePropertyRegex); + if (match) { + return match[1]; } - - const namePropertyRegex = /(?<=\S)\\.name/g; - const testNameWithoutNameProperty = receivedTestName.replace(namePropertyRegex, ''); - - const prototypePropertyRegex = /\w*\\.prototype\\./g; - return testNameWithoutNameProperty.replace(prototypePropertyRegex, ''); + return receivedTestName; }