├── .npmrc ├── .prettierrc ├── .eslintignore ├── public ├── icon.png └── vscode-jest.gif ├── .gitignore ├── examples ├── jest.config.js └── examples.test.ts ├── .vscodeignore ├── src ├── parser.ts ├── test │ ├── __mocks__ │ │ └── vscode.ts │ ├── utils.test.ts │ └── jestRunnerConfig.test.ts ├── JestRunnerCodeLensProvider.ts ├── extension.ts ├── util.ts ├── jestRunnerConfig.ts └── jestRunner.ts ├── jest.config.js ├── .eslintrc.js ├── tsconfig.json ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── .github └── workflows │ ├── pull_request.yml │ └── master.yml ├── LICENSE ├── webpack.config.js ├── CHANGELOG.md ├── README.md └── package.json /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 120 4 | } 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | coverage 4 | .vscode-test/ 5 | .vsix 6 | dist 7 | -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firsttris/vscode-jest-runner/HEAD/public/icon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | coverage 4 | .vscode-test/ 5 | *.vsix 6 | dist 7 | .eslintcache 8 | -------------------------------------------------------------------------------- /public/vscode-jest.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firsttris/vscode-jest-runner/HEAD/public/vscode-jest.gif -------------------------------------------------------------------------------- /examples/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; 5 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | src/** 5 | .gitignore 6 | tsconfig.json 7 | vsc-extension-quickstart.md 8 | node_modules/** 9 | webpack.config.js -------------------------------------------------------------------------------- /src/parser.ts: -------------------------------------------------------------------------------- 1 | import type { ParsedNode } from 'jest-editor-support'; 2 | import parse from 'jest-editor-support/build/parsers'; 3 | 4 | export { parse, ParsedNode }; 5 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | moduleNameMapper: { 5 | '^vscode$': 'src/test/__mocks__/vscode.ts', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { node: true }, 4 | parser: '@typescript-eslint/parser', 5 | plugins: ['@typescript-eslint'], 6 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', "plugin:prettier/recommended"], 7 | rules: { 8 | "prettier/prettier": "warn", 9 | 'no-template-curly-in-string': 'off', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "allowSyntheticDefaultImports": true 12 | }, 13 | "exclude": [ 14 | "node_modules", 15 | ".vscode-test", 16 | "examples" 17 | ] 18 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "dist": false 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true 10 | }, 11 | "editor.codeActionsOnSave": { 12 | "source.fixAll": "explicit", 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: pull_request 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | 7 | jobs: 8 | build: 9 | runs-on: ${{ matrix.platform }} 10 | 11 | strategy: 12 | matrix: 13 | node-version: [22.x] 14 | platform: [windows-latest, macos-latest, ubuntu-22.04] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm ci 23 | - run: npm run test 24 | - run: npm run build --if-present -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | name: master 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | build: 9 | runs-on: macos-latest 10 | 11 | strategy: 12 | matrix: 13 | node-version: [22.x] 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: npm ci 22 | - run: npm run test 23 | - run: npm run build --if-present 24 | - run: npm run publish 25 | env: 26 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 27 | OVSX_PAT: ${{ secrets.OVSX_PAT}} 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tristan Teufel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | /**@type {import('webpack').Configuration}*/ 8 | const config = { 9 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 10 | 11 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 12 | output: { 13 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 14 | path: path.resolve(__dirname, 'dist'), 15 | filename: 'extension.js', 16 | libraryTarget: 'commonjs2', 17 | devtoolModuleFilenameTemplate: '../[resource-path]', 18 | }, 19 | devtool: 'source-map', 20 | externals: { 21 | vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 22 | }, 23 | resolve: { 24 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 25 | extensions: ['.ts', '.js'], 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.ts$/, 31 | exclude: /(node_modules|test)/, 32 | use: [ 33 | { 34 | loader: 'ts-loader', 35 | }, 36 | ], 37 | }, 38 | ], 39 | }, 40 | }; 41 | module.exports = config; 42 | -------------------------------------------------------------------------------- /examples/examples.test.ts: -------------------------------------------------------------------------------- 1 | describe('Example tests', () => { 2 | it('test with ', () => { 3 | expect(true); 4 | }); 5 | 6 | it("test with ' single quote", () => { 7 | expect(true); 8 | }); 9 | 10 | it('test with " double quote', () => { 11 | expect(true); 12 | }); 13 | 14 | it('test with () parenthesis', () => { 15 | expect(true); 16 | }); 17 | 18 | it('test with [ square bracket', () => { 19 | expect(true); 20 | }); 21 | 22 | it(`test with 23 | lf`, () => { 24 | expect(true); 25 | }); 26 | 27 | it(`test with \nmanual lf`, () => { 28 | expect(true); 29 | }); 30 | 31 | it(`test with \r\nmanual crlf`, () => { 32 | expect(true); 33 | }); 34 | 35 | it('test with %var%', () => { 36 | expect(true); 37 | }); 38 | 39 | const v = 'interpolated string'; 40 | it(`test with ${v}`, () => { 41 | expect(true); 42 | }); 43 | 44 | it('test with $var', () => { 45 | expect(true); 46 | }); 47 | 48 | it('test with `backticks`', () => { 49 | expect(true); 50 | }); 51 | 52 | it('test with regex .*$^|[]', () => { 53 | expect(true); 54 | }); 55 | }); 56 | 57 | // #311 58 | it.each([1, 2])('test with generated %i', (id) => { 59 | expect(true); 60 | }); 61 | 62 | describe('nested', () => { 63 | describe('a', () => { 64 | it('b', () => { 65 | expect(true); 66 | }); 67 | }); 68 | }); 69 | 70 | // #299 71 | class TestClass { 72 | myFunction() { 73 | // nothing 74 | } 75 | } 76 | it(TestClass.prototype.myFunction.name, () => { 77 | expect(true).toBe(true); 78 | }); 79 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], 11 | "stopOnEntry": false, 12 | "sourceMaps": true, 13 | "outFiles": [ "${workspaceRoot}/dist/*.js" ], 14 | "preLaunchTask": "npm: build" 15 | }, 16 | { 17 | "name": "Extension examples", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "runtimeExecutable": "${execPath}", 21 | "args": [ 22 | "--extensionDevelopmentPath=${workspaceRoot}", 23 | "--folder-uri=${workspaceRoot}/examples", 24 | "--file-uri=${workspaceRoot}/examples/examples.test.ts" 25 | ], 26 | "stopOnEntry": false, 27 | "sourceMaps": true, 28 | "outFiles": [ "${workspaceRoot}/dist/*.js" ], 29 | "preLaunchTask": "npm: build" 30 | }, 31 | { 32 | "name": "Extension Tests", 33 | "type": "extensionHost", 34 | "request": "launch", 35 | "runtimeExecutable": "${execPath}", 36 | "args": [ 37 | "--extensionDevelopmentPath=${workspaceFolder}", 38 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 39 | ], 40 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"], 41 | "preLaunchTask": "${defaultBuildTask}" 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /src/test/__mocks__/vscode.ts: -------------------------------------------------------------------------------- 1 | // __mocks__/vscode.ts 2 | 3 | class Uri { 4 | constructor(readonly fsPath: string) {} 5 | } 6 | 7 | class Document { 8 | constructor(public readonly uri: Uri) {} 9 | } 10 | 11 | class TextEditor { 12 | constructor(public readonly document: Document) {} 13 | } 14 | 15 | class WorkspaceFolder { 16 | constructor(public readonly uri: Uri) {} 17 | 18 | name: string; 19 | index: number; 20 | } 21 | 22 | class Workspace { 23 | getWorkspaceFolder(uri: Uri): { uri: Uri } { 24 | return { uri }; 25 | } 26 | 27 | getConfiguration() { 28 | throw new WorkspaceConfiguration({}); 29 | } 30 | } 31 | 32 | type JestRunnerConfigProps = { 33 | 'jestrunner.projectPath'?: string; 34 | 'jestrunner.configPath'?: string | Record; 35 | 'jestrunner.useNearestConfig'?: boolean; 36 | 'jestrunner.checkRelativePathForJest'?: boolean; 37 | }; 38 | class WorkspaceConfiguration { 39 | constructor(private dict: JestRunnerConfigProps) {} 40 | 41 | get(key: T): (typeof this.dict)[T] { 42 | if (!(key in this.dict)) { 43 | throw new Error(`unrecognised config key ${key}`); 44 | } 45 | return this.dict[key]; 46 | } 47 | 48 | has(key: string) { 49 | return key in this.dict; 50 | } 51 | inspect(section: string): undefined { 52 | throw new Error('not implemented'); 53 | } 54 | update(key: string, value: string): Thenable { 55 | throw new Error('not implemented'); 56 | } 57 | } 58 | 59 | class Window { 60 | get activeTextEditor(): TextEditor { 61 | return new TextEditor(new Document(new Uri('hi'))); 62 | } 63 | showWarningMessage(message: string, ...items: T[]): Thenable { 64 | return Promise.resolve(undefined); 65 | } 66 | } 67 | 68 | const workspace = new Workspace(); 69 | const window = new Window(); 70 | 71 | export { workspace, window, Uri, Document, TextEditor, WorkspaceFolder, WorkspaceConfiguration }; 72 | -------------------------------------------------------------------------------- /src/JestRunnerCodeLensProvider.ts: -------------------------------------------------------------------------------- 1 | import { parse, ParsedNode } from './parser'; 2 | import { CodeLens, CodeLensProvider, Range, TextDocument, window, workspace } from 'vscode'; 3 | import { findFullTestName, escapeRegExp, CodeLensOption, normalizePath } from './util'; 4 | import { sync } from 'fast-glob'; 5 | 6 | function getCodeLensForOption(range: Range, codeLensOption: CodeLensOption, fullTestName: string): CodeLens { 7 | const titleMap: Record = { 8 | run: 'Run', 9 | debug: 'Debug', 10 | watch: 'Run --watch', 11 | coverage: 'Run --coverage', 12 | 'current-test-coverage': 'Run --collectCoverageFrom (target file/dir)' 13 | }; 14 | const commandMap: Record = { 15 | run: 'extension.runJest', 16 | debug: 'extension.debugJest', 17 | watch: 'extension.watchJest', 18 | coverage: 'extension.runJestCoverage', 19 | 'current-test-coverage': 'extension.runJestCurrentTestCoverage' 20 | }; 21 | return new CodeLens(range, { 22 | arguments: [fullTestName], 23 | title: titleMap[codeLensOption], 24 | command: commandMap[codeLensOption], 25 | }); 26 | } 27 | 28 | function getTestsBlocks( 29 | parsedNode: ParsedNode, 30 | parseResults: ParsedNode[], 31 | codeLensOptions: CodeLensOption[] 32 | ): CodeLens[] { 33 | const codeLens: CodeLens[] = []; 34 | 35 | parsedNode.children?.forEach((subNode) => { 36 | codeLens.push(...getTestsBlocks(subNode, parseResults, codeLensOptions)); 37 | }); 38 | 39 | const range = new Range( 40 | parsedNode.start.line - 1, 41 | parsedNode.start.column, 42 | parsedNode.end.line - 1, 43 | parsedNode.end.column 44 | ); 45 | 46 | if (parsedNode.type === 'expect') { 47 | return []; 48 | } 49 | 50 | const fullTestName = escapeRegExp(findFullTestName(parsedNode.start.line, parseResults)); 51 | 52 | codeLens.push(...codeLensOptions.map((option) => getCodeLensForOption(range, option, fullTestName))); 53 | 54 | return codeLens; 55 | } 56 | 57 | export class JestRunnerCodeLensProvider implements CodeLensProvider { 58 | private lastSuccessfulCodeLens: CodeLens[] = []; 59 | 60 | constructor(private readonly codeLensOptions: CodeLensOption[]) {} 61 | 62 | private get currentWorkspaceFolderPath(): string { 63 | const editor = window.activeTextEditor; 64 | return workspace.getWorkspaceFolder(editor.document.uri).uri.fsPath; 65 | } 66 | 67 | public async provideCodeLenses(document: TextDocument): Promise { 68 | try { 69 | const config = workspace.getConfiguration('jestrunner'); 70 | const include = config.get('include', []); 71 | const exclude = config.get('exclude', []); 72 | 73 | const filePath = normalizePath(document.fileName); 74 | const workspaceRoot = normalizePath(this.currentWorkspaceFolderPath); 75 | 76 | const globOptions = { cwd: workspaceRoot, absolute: true }; 77 | if (include.length > 0 && !sync(include, globOptions).includes(filePath)) { 78 | return []; 79 | } 80 | 81 | if (exclude.length > 0 && sync(exclude, globOptions).includes(filePath)) { 82 | return []; 83 | } 84 | 85 | const parseResults = parse(document.fileName, document.getText(), { plugins: { decorators: 'legacy' } }).root 86 | .children; 87 | this.lastSuccessfulCodeLens = parseResults.flatMap((parseResult) => 88 | getTestsBlocks(parseResult, parseResults, this.codeLensOptions) 89 | ); 90 | } catch (e) { 91 | console.error('jest-editor-support parser returned error', e); 92 | } 93 | return this.lastSuccessfulCodeLens; 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as vscode from 'vscode'; 3 | 4 | import { JestRunner } from './jestRunner'; 5 | import { JestRunnerCodeLensProvider } from './JestRunnerCodeLensProvider'; 6 | import { JestRunnerConfig } from './jestRunnerConfig'; 7 | 8 | export function activate(context: vscode.ExtensionContext): void { 9 | const config = new JestRunnerConfig(); 10 | const jestRunner = new JestRunner(config); 11 | const codeLensProvider = new JestRunnerCodeLensProvider(config.codeLensOptions); 12 | 13 | const runJest = vscode.commands.registerCommand( 14 | 'extension.runJest', 15 | async (argument: Record | string) => { 16 | return jestRunner.runCurrentTest(argument); 17 | } 18 | ); 19 | 20 | const runJestCoverage = vscode.commands.registerCommand( 21 | 'extension.runJestCoverage', 22 | async (argument: Record | string) => { 23 | return jestRunner.runCurrentTest(argument, ['--coverage']); 24 | } 25 | ); 26 | 27 | const runJestCurrentTestCoverage = vscode.commands.registerCommand( 28 | 'extension.runJestCurrentTestCoverage', 29 | async (argument: Record | string) => { 30 | return jestRunner.runCurrentTest(argument, ['--coverage'], true); 31 | } 32 | ); 33 | 34 | const runJestPath = vscode.commands.registerCommand('extension.runJestPath', async (argument: vscode.Uri) => 35 | jestRunner.runTestsOnPath(argument.fsPath) 36 | ); 37 | const runJestAndUpdateSnapshots = vscode.commands.registerCommand('extension.runJestAndUpdateSnapshots', async () => { 38 | jestRunner.runCurrentTest('', ['-u']); 39 | }); 40 | const runJestFile = vscode.commands.registerCommand('extension.runJestFile', async () => jestRunner.runCurrentFile()); 41 | const debugJest = vscode.commands.registerCommand( 42 | 'extension.debugJest', 43 | async (argument: Record | string) => { 44 | if (typeof argument === 'string') { 45 | return jestRunner.debugCurrentTest(argument); 46 | } else { 47 | return jestRunner.debugCurrentTest(); 48 | } 49 | } 50 | ); 51 | const debugJestPath = vscode.commands.registerCommand('extension.debugJestPath', async (argument: vscode.Uri) => 52 | jestRunner.debugTestsOnPath(argument.fsPath) 53 | ); 54 | const runPrev = vscode.commands.registerCommand('extension.runPrevJest', async () => jestRunner.runPreviousTest()); 55 | const runJestFileWithCoverage = vscode.commands.registerCommand('extension.runJestFileWithCoverage', async () => 56 | jestRunner.runCurrentFile(['--coverage']) 57 | ); 58 | 59 | const runJestFileWithWatchMode = vscode.commands.registerCommand('extension.runJestFileWithWatchMode', async () => 60 | jestRunner.runCurrentFile(['--watch']) 61 | ); 62 | 63 | const watchJest = vscode.commands.registerCommand( 64 | 'extension.watchJest', 65 | async (argument: Record | string) => { 66 | return jestRunner.runCurrentTest(argument, ['--watch']); 67 | } 68 | ); 69 | 70 | if (!config.isCodeLensDisabled) { 71 | const docSelectors: vscode.DocumentFilter[] = [ 72 | { 73 | pattern: vscode.workspace.getConfiguration().get('jestrunner.codeLensSelector'), 74 | }, 75 | ]; 76 | const codeLensProviderDisposable = vscode.languages.registerCodeLensProvider(docSelectors, codeLensProvider); 77 | context.subscriptions.push(codeLensProviderDisposable); 78 | } 79 | context.subscriptions.push(runJest); 80 | context.subscriptions.push(runJestCoverage); 81 | context.subscriptions.push(runJestCurrentTestCoverage); 82 | context.subscriptions.push(runJestAndUpdateSnapshots); 83 | context.subscriptions.push(runJestFile); 84 | context.subscriptions.push(runJestPath); 85 | context.subscriptions.push(debugJest); 86 | context.subscriptions.push(debugJestPath); 87 | context.subscriptions.push(runPrev); 88 | context.subscriptions.push(runJestFileWithCoverage); 89 | context.subscriptions.push(runJestFileWithWatchMode); 90 | context.subscriptions.push(watchJest); 91 | } 92 | 93 | export function deactivate(): void { 94 | // deactivate 95 | } 96 | -------------------------------------------------------------------------------- /src/test/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { isWindows, searchPathToParent, validateCodeLensOptions } from '../util'; 2 | import * as fs from 'fs'; 3 | 4 | const its = { 5 | windows: isWindows() ? it : it.skip, 6 | linux: ['linux', 'darwin'].includes(process.platform) ? it : it.skip, 7 | }; 8 | 9 | describe('validateCodeLensOptions', () => 10 | it.each([ 11 | [ 12 | ['a', 'run', 'RUN', 'watch', 'debug', 'other', 'debug', 'debug', 'watch', 'run'], 13 | ['run', 'watch', 'debug'], 14 | ], 15 | [[], []], 16 | ])('should turn "jestrunner.codeLens" options into something valid', (input, expected) => { 17 | expect(validateCodeLensOptions(input)).toEqual(expected); 18 | })); 19 | 20 | describe('searchPathToParent', () => { 21 | const scenarios: Array< 22 | [os: string, fileAsStartPath: string, folderAsStartPath: string, workspacePath: string, traversedPaths: string[]] 23 | > = [ 24 | [ 25 | 'linux', 26 | '/home/user/workspace/package/src/file.ts', 27 | '/home/user/workspace/package/src', 28 | '/home/user/workspace', 29 | ['/home/user/workspace/package/src', '/home/user/workspace/package', '/home/user/workspace'], 30 | ], 31 | [ 32 | 'windows', 33 | 'C:\\Users\\user\\workspace\\package\\src\\file.ts', 34 | 'C:\\Users\\user\\workspace\\package\\src', 35 | 'C:\\Users\\user\\workspace', 36 | ['C:\\Users\\user\\workspace\\package\\src', 'C:\\Users\\user\\workspace\\package', 'C:\\Users\\user\\workspace'], 37 | ], 38 | ]; 39 | describe.each(scenarios)('on %s', (os, fileAsStartPath, folderAsStartPath, workspacePath, traversedPaths) => { 40 | // const fileAsStartPath = '/home/user/workspace/package/src/file.ts'; 41 | // const folderAsStartPath = '/home/user/workspace/package/src'; 42 | // const workspacePath = '/home/user/workspace'; 43 | // const traversedPaths = ['/home/user/workspace/package/src', '/home/user/workspace/package', '/home/user/workspace']; 44 | beforeEach(() => { 45 | jest.spyOn(fs, 'statSync').mockImplementation((path): any => { 46 | if (path === fileAsStartPath) { 47 | return { isFile: () => true, isDirectory: () => false }; 48 | } 49 | return { isFile: () => false, isDirectory: () => true }; 50 | }); 51 | }); 52 | 53 | its[os]('starts traversal at the starting (directory) path', () => { 54 | const mockCallback = jest.fn().mockReturnValue('found'); 55 | searchPathToParent(folderAsStartPath, workspacePath, mockCallback); 56 | expect(mockCallback).toHaveBeenCalledTimes(1); 57 | expect(mockCallback).toHaveBeenCalledWith(traversedPaths[0]); 58 | }); 59 | its[os]('starts traversal at the folder of the starting (file) path', () => { 60 | const mockCallback = jest.fn().mockReturnValue('found'); 61 | searchPathToParent(fileAsStartPath, workspacePath, mockCallback); 62 | expect(mockCallback).toHaveBeenCalledTimes(1); 63 | expect(mockCallback).toHaveBeenCalledWith(traversedPaths[0]); 64 | }); 65 | its[os]('traverses up to and includes the ancestor path', () => { 66 | const mockCallback = jest.fn().mockReturnValue(false); 67 | searchPathToParent(fileAsStartPath, workspacePath, mockCallback); 68 | expect(mockCallback).toHaveBeenCalledTimes(traversedPaths.length); 69 | for (const path of traversedPaths) { 70 | expect(mockCallback).toHaveBeenCalledWith(path); 71 | } 72 | }); 73 | its[os]('continues traversal if callback returns 0', () => { 74 | const mockCallback = jest.fn().mockReturnValue(0); 75 | const result = searchPathToParent(fileAsStartPath, workspacePath, mockCallback); 76 | expect(result).toBe(false); 77 | }); 78 | its[os]('continues traversal if callback returns null', () => { 79 | const mockCallback = jest.fn().mockReturnValue(null); 80 | const result = searchPathToParent(fileAsStartPath, workspacePath, mockCallback); 81 | expect(result).toBe(false); 82 | }); 83 | its[os]('continues traversal if callback returns void (undefined)', () => { 84 | const mockCallback = jest.fn().mockReturnValue(undefined); 85 | const result = searchPathToParent(fileAsStartPath, workspacePath, mockCallback); 86 | expect(result).toBe(false); 87 | }); 88 | its[os]('continues traversal if callback returns false', () => { 89 | const mockCallback = jest.fn().mockReturnValue(undefined); 90 | const result = searchPathToParent(fileAsStartPath, workspacePath, mockCallback); 91 | expect(result).toBe(false); 92 | }); 93 | its[os]('it stops traversal when the callback returns a string', () => { 94 | const mockCallback = jest.fn().mockReturnValueOnce(false).mockReturnValue('found'); 95 | searchPathToParent(fileAsStartPath, workspacePath, mockCallback); 96 | expect(mockCallback).toHaveBeenCalledTimes(2); 97 | expect(mockCallback).toHaveBeenCalledWith(traversedPaths[0]); 98 | expect(mockCallback).toHaveBeenCalledWith(traversedPaths[1]); 99 | }); 100 | its[os]('returns the non-falsy value returned by the callback', () => { 101 | const mockCallback = jest.fn().mockReturnValueOnce(false).mockReturnValue('found'); 102 | const result = searchPathToParent(fileAsStartPath, workspacePath, mockCallback); 103 | expect(result).toBe('found'); 104 | }); 105 | its[os]('returns false if the traversal completes without the callback returning a string', () => { 106 | const mockCallback = jest.fn().mockReturnValue(false); 107 | const result = searchPathToParent(fileAsStartPath, workspacePath, mockCallback); 108 | expect(result).toBe(false); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 0.4.52 - 2022-08-14 4 | 5 | - Let CI continously publish extension to ovsx 6 | - Refactor jestRunner.ts to inject config in constructor #263 7 | 8 | ## 0.4.48 - 2022-08-14 9 | 10 | - Github Actions CI building the addon continously for changes on master #246 11 | - support for external native terminal run #258 12 | 13 | ### Added 14 | 15 | ## 0.4.48 - 2022-03-09 16 | 17 | ### Added 18 | 19 | - Respect "Change Directory To Workspace Root" setting when in Debug mode (#194) #224 20 | - feat(codelens): add options to display desired codelens #213 21 | - fix: missing import for CodeLensOption #232 22 | - Add basic CI using GitHub Actions #236 23 | - Adds yarnPnpCommand to config #231 24 | - Update jest-editor-support, fix CI #239 25 | - fix: valid jestBinPath is js file #244 26 | - Update jest-editor-support and only import parser #240 27 | 28 | ## 0.4.47 - 2021-09-09 29 | 30 | ### Added 31 | 32 | - add watch mode on codelens (#204) 33 | - Support searching for Typescript configuration files in monorepo environments (#195) 34 | - Add config option to preserve editor focus on test run (#205) 35 | 36 | ### Fixed 37 | 38 | - Fix path for yarn 2 release file (#197) 39 | - Fix explorer menus not showing (#184) 40 | 41 | ## 0.4.44 - 2021-06-12 42 | 43 | ### Fixed 44 | 45 | - escape yarn version for pnp 46 | 47 | ## 0.4.43 - 2021-06-12 48 | 49 | ### Fixed 50 | 51 | - rest debug arguments back to default 52 | 53 | ## 0.4.41 - 2021-06-12 54 | 55 | ### Fixed 56 | 57 | - simplyfied yarn pnp implementation, removed jestrunner.detectYarnPnpJestBin config option 58 | 59 | ## 0.4.40 - 2021-06-12 60 | 61 | ### Fixed 62 | 63 | - updated jest-editor-support to v29 64 | - optimized escaping of filepath 65 | 66 | ## 0.4.39 - 2021-06-03 67 | 68 | ### Added 69 | 70 | - extended readme.md with how to debug tsx/jsx 71 | 72 | ## 0.4.37 - 2021-06-03 73 | 74 | ### Fixed 75 | 76 | - wrong escaping of filename 77 | 78 | ## 0.4.35 - 2021-06-02 79 | 80 | ### Added 81 | 82 | - Add explorer menus to run and debug tests with jest #149 83 | - chore: use eslint vs. tslint #178 84 | 85 | ### Fixed 86 | 87 | - escape special characters in test filepath (#114) #162 88 | - Keep Run/Debug buttons shown when failed to parse document #163 89 | 90 | ## 0.4.34 - 2021-04-11 91 | 92 | ### Fixed 93 | 94 | - auto detect project path with jest binary #156 95 | - add option to command palette to update snapshots #152 96 | - remove run jest file from context menu (still available in command palette strg + shift + p) 97 | - add option changeDirectoryToWorkspaceRoot to enable/disable changing directory to workspace root before executing the test 98 | 99 | ## 0.4.33 - 2021-04-06 100 | 101 | ### Fixed 102 | 103 | - resolve jest test name string interpolation for test pattern such as test.each #148 104 | - Parameterised test support and deps cleanup #146 105 | - Escape plus sign in file path #140 106 | 107 | ## 0.4.31 - 2020-10-26 108 | 109 | ### Fixed 110 | 111 | - Escape single quotes on Testnames (Linux) 112 | 113 | ## 0.4.29 - 2020-10-26 114 | 115 | ### Remove 116 | 117 | - Dynamic resolution of ProjectPath 118 | 119 | ## 0.4.28 - 2020-10-24 120 | 121 | ### Added 122 | 123 | - Dynamic resolution of ProjectPath 124 | 125 | ## 0.4.27 - 2020-10-14 126 | 127 | ### Added 128 | 129 | - Dynamic Jest config (jest.config.js) resolution (monorepo) 130 | 131 | ## 0.4.24 - 2020-09-16 132 | 133 | ### Added 134 | 135 | - Yarn 2 Pnp Support 136 | - Ability to define fixed ProjectPath instead of workspaceFolder (monorepo) 137 | 138 | ## 0.4.22 - 2020-06-05 139 | 140 | ### Added 141 | 142 | - add new settings codeLensSelector which enables CodeLens for files matching this pattern (default **/*.{test,spec}.{js,jsx,ts,tsx}) 143 | 144 | ## 0.4.21 - 2020-06-02 145 | 146 | ### Added 147 | 148 | - debug Jest CodeLens option 149 | 150 | ## 0.4.20 - 2020-06-02 151 | 152 | ### Added 153 | 154 | - change CodeLens filename pattern to *.test.ts,*.test.js,*.test.tsx,*.test.jsx 155 | - add Settings to disable CodeLens 156 | 157 | ## 0.4.19 - 2020-05-28 158 | 159 | ### Added 160 | 161 | - CodeLens option to start Tests 162 | 163 | ## 0.4.18 - 2020-05-22 164 | 165 | ### Added 166 | 167 | - Added option to run jest file with coverage to the command palette 168 | 169 | ### Fixed 170 | - Reduced bundle size 171 | - Run test outside a describe block 172 | 173 | ## 0.4.17 - 2020-05-07 174 | 175 | ### Fixed 176 | 177 | - powershell not working due to && 178 | 179 | ## 0.4.16 - 2020-05-07 180 | 181 | ### Fixed 182 | 183 | - run previous test is just cd-ing when last run was debug 184 | 185 | ## 0.4.12 - 2020-01-20 186 | 187 | ### Fixed 188 | 189 | - remove ExactRegexMatch approach due to to many issues 190 | - fix Commandline on Windows10 191 | 192 | ## 0.4.11 - 2019-12-20 193 | 194 | ### Fixed 195 | 196 | - Ability to start Test by clicking anywhere inside the line-range 197 | 198 | ### Changed 199 | 200 | - Changed build to Webpack bundle 201 | 202 | ## 0.4.9 - 2019-12-16 203 | 204 | ### Added 205 | 206 | - integrated jest parser of jest-editor-support 207 | - Warning about incorrect config 208 | 209 | ## 0.4.7 - 2019-12-14 210 | 211 | ### Fixed 212 | 213 | - Fix dependency Issue 214 | 215 | ## 0.4.5 - 2019-12-13 216 | 217 | ### Changed 218 | 219 | - Removed icon from context menu entry 220 | 221 | ## 0.4.4 - 2019-12-13 222 | 223 | ### Added 224 | 225 | - Add ability to add CLI Options to run Jest command with jestrunner.runOptions 226 | 227 | ## 0.4.3 - 2019-12-13 228 | 229 | ### Fixed 230 | 231 | - Support for Workspaces for Run and Debug Mode 232 | - Overlapping Test Names 233 | - Escape Special Characters 234 | 235 | ## 0.4.2 - 2019-10-19 236 | 237 | ### Changed 238 | 239 | - Extended Readme. 240 | 241 | ## 0.4.0 - 2019-10-19 242 | 243 | ### Added 244 | 245 | - Context menu icon. 246 | 247 | ### Changed 248 | 249 | - Deprecated `jestrunner.runOptions` option in favor of `jestrunner.debugOptions`. 250 | 251 | ### Fixed 252 | 253 | - Debug Jest fails for CMD and GIT bash ([#38](https://github.com/firsttris/vscode-jest-runner/issues/38)). 254 | 255 | ## 0.0.1 - 2017-12-29 256 | 257 | - Initial release 258 | 259 | --- 260 | 261 | The file format is based on [Keep a Changelog](http://keepachangelog.com/). 262 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { execSync } from 'child_process'; 3 | import * as mm from 'micromatch'; 4 | import * as vscode from 'vscode'; 5 | import * as fs from 'fs'; 6 | 7 | export function getDirName(filePath: string): string { 8 | return path.dirname(filePath); 9 | } 10 | 11 | export function getFileName(filePath: string): string { 12 | return path.basename(filePath); 13 | } 14 | 15 | export function isWindows(): boolean { 16 | return process.platform.includes('win32'); 17 | } 18 | 19 | export function normalizePath(path: string): string { 20 | return isWindows() ? path.replace(/\\/g, '/') : path; 21 | } 22 | 23 | export function escapeRegExp(s: string): string { 24 | const escapedString = s.replace(/[.*+?^${}<>()|[\]\\]/g, '\\$&'); // $& means the whole matched string 25 | return escapedString.replace(/\\\(\\\.\\\*\\\?\\\)/g, '(.*?)'); // should revert the escaping of match all regex patterns. 26 | } 27 | 28 | export function escapeRegExpForPath(s: string): string { 29 | return s.replace(/[*+?^${}<>()|[\]]/g, '\\$&'); // $& means the whole matched string 30 | } 31 | 32 | export function findFullTestName(selectedLine: number, children: any[]): string | undefined { 33 | if (!children) { 34 | return; 35 | } 36 | for (const element of children) { 37 | if (element.type === 'describe' && selectedLine === element.start.line) { 38 | return resolveTestNameStringInterpolation(element.name); 39 | } 40 | if (element.type !== 'describe' && selectedLine >= element.start.line && selectedLine <= element.end.line) { 41 | return resolveTestNameStringInterpolation(element.name); 42 | } 43 | } 44 | for (const element of children) { 45 | const result = findFullTestName(selectedLine, element.children); 46 | if (result) { 47 | return resolveTestNameStringInterpolation(element.name) + ' ' + result; 48 | } 49 | } 50 | } 51 | 52 | const QUOTES = { 53 | '"': true, 54 | "'": true, 55 | '`': true, 56 | }; 57 | 58 | function resolveTestNameStringInterpolation(s: string): string { 59 | const variableRegex = /(\${?[A-Za-z0-9_]+}?|%[psdifjo#%])/gi; 60 | const matchAny = '(.*?)'; 61 | return s.replace(variableRegex, matchAny); 62 | } 63 | 64 | export function escapeSingleQuotes(s: string): string { 65 | return isWindows() ? s : s.replace(/'/g, "'\\''"); 66 | } 67 | 68 | export function quote(s: string): string { 69 | const q = isWindows() ? '"' : `'`; 70 | return [q, s, q].join(''); 71 | } 72 | 73 | export function unquote(s: string): string { 74 | if (QUOTES[s[0]]) { 75 | s = s.substring(1); 76 | } 77 | 78 | if (QUOTES[s[s.length - 1]]) { 79 | s = s.substring(0, s.length - 1); 80 | } 81 | 82 | return s; 83 | } 84 | 85 | export function pushMany(arr: T[], items: T[]): number { 86 | return Array.prototype.push.apply(arr, items); 87 | } 88 | 89 | export type CodeLensOption = 'run' | 'debug' | 'watch' | 'coverage' | 'current-test-coverage'; 90 | 91 | function isCodeLensOption(option: string): option is CodeLensOption { 92 | return ['run', 'debug', 'watch', 'coverage', 'current-test-coverage'].includes(option); 93 | } 94 | 95 | export function validateCodeLensOptions(maybeCodeLensOptions: string[]): CodeLensOption[] { 96 | return [...new Set(maybeCodeLensOptions)].filter((value) => isCodeLensOption(value)) as CodeLensOption[]; 97 | } 98 | 99 | export function isNodeExecuteAbleFile(filepath: string): boolean { 100 | try { 101 | execSync(`node ${filepath} --help`); 102 | return true; 103 | } catch (err) { 104 | return false; 105 | } 106 | } 107 | 108 | export function updateTestNameIfUsingProperties(receivedTestName?: string) { 109 | if (receivedTestName === undefined) { 110 | return undefined; 111 | } 112 | 113 | const namePropertyRegex = /(?<=\S)\\.name/g; 114 | const testNameWithoutNameProperty = receivedTestName.replace(namePropertyRegex, ''); 115 | 116 | const prototypePropertyRegex = /\w*\\.prototype\\./g; 117 | return testNameWithoutNameProperty.replace(prototypePropertyRegex, ''); 118 | } 119 | 120 | export function resolveConfigPathOrMapping( 121 | configPathOrMapping: string | Record | undefined, 122 | targetPath: string, 123 | ): string | undefined { 124 | if (['string', 'undefined'].includes(typeof configPathOrMapping)) { 125 | return configPathOrMapping as string | undefined; 126 | } 127 | for (const [key, value] of Object.entries(configPathOrMapping as Record)) { 128 | const isMatch = mm.matcher(key); 129 | // try the glob against normalized and non-normalized path 130 | if (isMatch(targetPath) || isMatch(normalizePath(targetPath))) { 131 | return normalizePath(value); 132 | } 133 | } 134 | if (Object.keys(configPathOrMapping).length > 0) { 135 | vscode.window.showWarningMessage( 136 | `None of the glob patterns in the configPath mapping matched the target file. Make sure you're using correct glob pattern syntax. Jest-runner uses the same library (micromatch) for evaluating glob patterns as Jest uses to evaluate it's 'testMatch' configuration.`, 137 | ); 138 | } 139 | 140 | return undefined; 141 | } 142 | 143 | /** 144 | * Traverse from starting path to and including ancestor path calling the callback function with each path. 145 | * If the callback function returns a non-falsy value, the traversal will stop and the value will be returned. 146 | * Returns false if the traversal completes without the callback returning a non-false value. 147 | * @param ancestorPath 148 | * @param startingPath 149 | * @param callback (currentFolderPath: string) => false | T 150 | */ 151 | export function searchPathToParent( 152 | startingPath: string, 153 | ancestorPath: string, 154 | callback: (currentFolderPath: string) => false | undefined | null | 0 | T, 155 | ) { 156 | let currentFolderPath = fs.statSync(startingPath).isDirectory() ? startingPath : path.dirname(startingPath); 157 | const endPath = path.dirname(ancestorPath); 158 | const resolvedStart = path.resolve(currentFolderPath); 159 | const resolvedEnd = path.resolve(endPath); 160 | // this might occur if you've opened a file outside of the workspace 161 | if (!resolvedStart.startsWith(resolvedEnd)) { 162 | return false; 163 | } 164 | 165 | // prevent edge case of workdir at root path ie, '/' -> '..' -> '/' 166 | let lastPath: null | string = null; 167 | do { 168 | const result = callback(currentFolderPath); 169 | if (result) { 170 | return result; 171 | } 172 | lastPath = currentFolderPath; 173 | currentFolderPath = path.dirname(currentFolderPath); 174 | } while (currentFolderPath !== endPath && currentFolderPath !== lastPath); 175 | 176 | return false; 177 | } 178 | -------------------------------------------------------------------------------- /src/jestRunnerConfig.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | import * as vscode from 'vscode'; 4 | import { 5 | normalizePath, 6 | quote, 7 | validateCodeLensOptions, 8 | CodeLensOption, 9 | isNodeExecuteAbleFile, 10 | resolveConfigPathOrMapping, 11 | searchPathToParent, 12 | } from './util'; 13 | 14 | export class JestRunnerConfig { 15 | /** 16 | * The command that runs jest. 17 | * Defaults to: node "node_modules/.bin/jest" 18 | */ 19 | public get jestCommand(): string { 20 | // custom 21 | const jestCommand: string = vscode.workspace.getConfiguration().get('jestrunner.jestCommand'); 22 | if (jestCommand) { 23 | return jestCommand; 24 | } 25 | 26 | // default 27 | if (this.isYarnPnpSupportEnabled) { 28 | return `yarn jest`; 29 | } 30 | return `node ${quote(this.jestBinPath)}`; 31 | } 32 | 33 | public get changeDirectoryToWorkspaceRoot(): boolean { 34 | return vscode.workspace.getConfiguration().get('jestrunner.changeDirectoryToWorkspaceRoot'); 35 | } 36 | 37 | public get preserveEditorFocus(): boolean { 38 | return vscode.workspace.getConfiguration().get('jestrunner.preserveEditorFocus') || false; 39 | } 40 | 41 | public get jestBinPath(): string { 42 | // custom 43 | let jestPath: string = vscode.workspace.getConfiguration().get('jestrunner.jestPath'); 44 | if (jestPath) { 45 | return jestPath; 46 | } 47 | 48 | // default 49 | const fallbackRelativeJestBinPath = 'node_modules/jest/bin/jest.js'; 50 | const mayRelativeJestBin = ['node_modules/.bin/jest', 'node_modules/jest/bin/jest.js']; 51 | const cwd = this.cwd; 52 | 53 | jestPath = mayRelativeJestBin.find((relativeJestBin) => isNodeExecuteAbleFile(path.join(cwd, relativeJestBin))); 54 | jestPath = jestPath || path.join(cwd, fallbackRelativeJestBinPath); 55 | 56 | return normalizePath(jestPath); 57 | } 58 | 59 | public get cwd(): string { 60 | return this.projectPathFromConfig || this.currentPackagePath || this.currentWorkspaceFolderPath; 61 | } 62 | 63 | private get projectPathFromConfig(): string | undefined { 64 | const projectPathFromConfig = vscode.workspace.getConfiguration().get('jestrunner.projectPath'); 65 | if (projectPathFromConfig) { 66 | return path.resolve(this.currentWorkspaceFolderPath, projectPathFromConfig); 67 | } 68 | } 69 | 70 | private get useNearestConfig(): boolean | undefined { 71 | return vscode.workspace.getConfiguration().get('jestrunner.useNearestConfig'); 72 | } 73 | 74 | public get currentPackagePath() { 75 | const checkRelativePathForJest = vscode.workspace 76 | .getConfiguration() 77 | .get('jestrunner.checkRelativePathForJest'); 78 | const foundPath = searchPathToParent( 79 | path.dirname(vscode.window.activeTextEditor.document.uri.fsPath), 80 | this.currentWorkspaceFolderPath, 81 | (currentFolderPath: string) => { 82 | // Try to find where jest is installed relatively to the current opened file. 83 | // Do not assume that jest is always installed at the root of the opened project, this is not the case 84 | // such as in multi-module projects. 85 | const pkg = path.join(currentFolderPath, 'package.json'); 86 | const jest = path.join(currentFolderPath, 'node_modules', 'jest'); 87 | if (fs.existsSync(pkg) && (fs.existsSync(jest) || !checkRelativePathForJest)) { 88 | return currentFolderPath; 89 | } 90 | }, 91 | ); 92 | return foundPath ? normalizePath(foundPath) : ''; 93 | } 94 | 95 | private get currentWorkspaceFolderPath(): string { 96 | const editor = vscode.window.activeTextEditor; 97 | return vscode.workspace.getWorkspaceFolder(editor.document.uri).uri.fsPath; 98 | } 99 | 100 | public getJestConfigPath(targetPath: string): string { 101 | // custom 102 | const configPathOrMapping: string | Record | undefined = vscode.workspace 103 | .getConfiguration() 104 | .get('jestrunner.configPath'); 105 | 106 | const configPath = resolveConfigPathOrMapping(configPathOrMapping, targetPath); 107 | if (!configPath || this.useNearestConfig) { 108 | const foundPath = this.findConfigPath(targetPath, configPath); 109 | if (foundPath) { 110 | return foundPath; 111 | // Continue to default if no config is found 112 | } 113 | } 114 | 115 | // default 116 | return configPath 117 | ? normalizePath(path.resolve(this.currentWorkspaceFolderPath, this.projectPathFromConfig || '', configPath)) 118 | : ''; 119 | } 120 | 121 | public findConfigPath(targetPath?: string, targetConfigFilename?: string): string | undefined { 122 | const foundPath = searchPathToParent( 123 | targetPath || path.dirname(vscode.window.activeTextEditor.document.uri.fsPath), 124 | this.currentWorkspaceFolderPath, 125 | (currentFolderPath: string) => { 126 | for (const configFilename of targetConfigFilename 127 | ? [targetConfigFilename] 128 | : ['jest.config.js', 'jest.config.ts', 'jest.config.cjs', 'jest.config.mjs', 'jest.config.json']) { 129 | const currentFolderConfigPath = path.join(currentFolderPath, configFilename); 130 | 131 | if (fs.existsSync(currentFolderConfigPath)) { 132 | return currentFolderConfigPath; 133 | } 134 | } 135 | }, 136 | ); 137 | return foundPath ? normalizePath(foundPath) : undefined; 138 | } 139 | 140 | public get runOptions(): string[] | null { 141 | const runOptions = vscode.workspace.getConfiguration().get('jestrunner.runOptions'); 142 | if (runOptions) { 143 | if (Array.isArray(runOptions)) { 144 | return runOptions; 145 | } else { 146 | vscode.window.showWarningMessage( 147 | 'Please check your vscode settings. "jestrunner.runOptions" must be an Array. ', 148 | ); 149 | } 150 | } 151 | return null; 152 | } 153 | 154 | public get debugOptions(): Partial { 155 | const debugOptions = vscode.workspace.getConfiguration().get('jestrunner.debugOptions'); 156 | if (debugOptions) { 157 | return debugOptions; 158 | } 159 | 160 | // default 161 | return {}; 162 | } 163 | 164 | public get isCodeLensDisabled(): boolean { 165 | const isCodeLensDisabled: boolean = vscode.workspace.getConfiguration().get('jestrunner.disableCodeLens'); 166 | return isCodeLensDisabled ? isCodeLensDisabled : false; 167 | } 168 | 169 | public get isRunInExternalNativeTerminal(): boolean { 170 | const isRunInExternalNativeTerminal: boolean = vscode.workspace 171 | .getConfiguration() 172 | .get('jestrunner.runInOutsideTerminal'); 173 | return isRunInExternalNativeTerminal ? isRunInExternalNativeTerminal : false; 174 | } 175 | 176 | public get codeLensOptions(): CodeLensOption[] { 177 | const codeLensOptions = vscode.workspace.getConfiguration().get('jestrunner.codeLens'); 178 | if (Array.isArray(codeLensOptions)) { 179 | return validateCodeLensOptions(codeLensOptions); 180 | } 181 | return []; 182 | } 183 | 184 | public get isYarnPnpSupportEnabled(): boolean { 185 | const isYarnPnp: boolean = vscode.workspace.getConfiguration().get('jestrunner.enableYarnPnpSupport'); 186 | return isYarnPnp ? isYarnPnp : false; 187 | } 188 | public get getYarnPnpCommand(): string { 189 | const yarnPnpCommand: string = vscode.workspace.getConfiguration().get('jestrunner.yarnPnpCommand'); 190 | return yarnPnpCommand; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vscode-jest-runner 2 | 3 | Looking for collaborators to help me maintain the project. Please contact me at tristanteufel@gmail.com 4 | 5 | ## Visual Studio Code Marketplace 6 | 7 | [VisualStudio Marketplace](https://marketplace.visualstudio.com/items?itemName=firsttris.vscode-jest-runner) 8 | [Open VSX Registry](https://open-vsx.org/extension/firsttris/vscode-jest-runner) 9 | 10 | ## Comparison with [vscode-jest](https://github.com/jest-community/vscode-jest) 11 | 12 | [vscode-jest-runner](https://github.com/firsttris/vscode-jest-runner) is focused on running or debugging a specific test or test-suite, while [vscode-jest](https://github.com/jest-community/vscode-jest) is running your current test-suite everytime you change it. 13 | 14 | ## Features 15 | 16 | Simple way to run or debug a specific test 17 | *As it is possible in IntelliJ / Webstorm* 18 | 19 | Run & Debug your Jest Tests from 20 | - Context-Menu 21 | - CodeLens 22 | - Command Palette (strg+shift+p) 23 | 24 | ## Supports 25 | - yarn & vscode workspaces (monorepo) 26 | - dynamic jest config resolution 27 | - yarn 2 pnp 28 | - CRA & and similar abstractions 29 | 30 | ![Extension Example](https://github.com/firsttris/vscode-jest/raw/master/public/vscode-jest.gif) 31 | 32 | ## Usage with CRA or similar abstractions 33 | 34 | add the following command to settings: 35 | ```json 36 | "jestrunner.jestCommand": "npm run test --", 37 | "jestrunner.debugOptions": { 38 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/react-scripts", 39 | "runtimeArgs": [ 40 | "test", 41 | "${fileBasename}", 42 | "--runInBand", 43 | "--no-cache", 44 | "--watchAll=false", 45 | "--color" 46 | ] 47 | }, 48 | ``` 49 | 50 | ## Usage with nvm 51 | 52 | add the following command to settings to help jestrunner find your node: 53 | ```json 54 | "jestrunner.jestCommand": "nvm use && npm run test --", 55 | "jestrunner.debugOptions": { 56 | runtimeExecutable": "/PATH/TO/YOUR/node" 57 | }, 58 | ``` 59 | 60 | ## Extension Settings 61 | 62 | Jest Runner will work out of the box, with a valid Jest config. 63 | If you have a custom setup use the following options to customize Jest Runner: 64 | 65 | | Command | Description | 66 | | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | 67 | | jestrunner.configPath | Jest config path (string) (relative to `${workspaceFolder}` e.g. jest-config.json). Defaults to blank. Can also be a glob path mapping. See [below](#configpath-as-glob-map) for more details | 68 | | jestrunner.useNearestConfig | If jestrunner.configPath is set and this is `true`, the nearest matching jest config up the path hierarchy will be used instead of the relative path. | 69 | | jestrunner.jestPath | Absolute path to jest bin file (e.g. /usr/lib/node_modules/jest/bin/jest.js) | 70 | | jestrunner.debugOptions | Add or overwrite vscode debug configurations (only in debug mode) (e.g. `"jestrunner.debugOptions": { "args": ["--no-cache"] }`) See [vscode docs](https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_launch-configuration-attributes) | 71 | | jestrunner.runOptions | Add CLI Options to the Jest Command (e.g. `"jestrunner.runOptions": ["--coverage", "--colors"]`) https://jestjs.io/docs/en/cli | 72 | | jestrunner.jestCommand | Define an alternative Jest command (e.g. for Create React App and similar abstractions) | 73 | | jestrunner.disableCodeLens | Disable CodeLens feature | 74 | | jestrunner.codeLensSelector | CodeLens will be shown on files matching this pattern (default **/*.{test,spec}.{js,jsx,ts,tsx}) | 75 | | jestrunner.codeLens | Choose which CodeLens to enable, default to `["run", "debug"]` | 76 | | jestrunner.enableYarnPnpSupport | Enable if you are using Yarn 2 with Plug'n'Play | 77 | | jestrunner.yarnPnpCommand | Command for debugging with Plug'n'Play defaults to yarn-*.*js | 78 | | jestrunner.projectPath | Absolute path to project directory (e.g. /home/me/project/sub-folder), or relative path from workspace root (e.g. ./sub-folder) | 79 | | jestrunner.checkRelativePathForJest | When `false`, when looking for the nearest resolution for Jest, only check for package.json files, rather than the `node_modules` folder. Default: `true` | 80 | | jestrunner.changeDirectoryToWorkspaceRoot | Changes directory before execution. The order is:
  1. `jestrunner.projectPath`
  2. the nearest `package.json`
  3. `${workspaceFolder}`
| 81 | | jestrunner.preserveEditorFocus | Preserve focus on your editor instead of focusing the terminal on test run | 82 | | jestrunner.runInExternalNativeTerminal | run in external terminal (requires: npm install ttab -g) | 83 | 84 | ### configPath as glob map 85 | If you've got multiple jest configs for running tests (ie maybe a config for unit tests, integration tests and frontend tests) then this option is for you. You can provide a map of glob matchers to specify which jest config to use based on the name of the file the test is being run for. 86 | 87 | For instance, supose you're using the naming convention of `*.spec.ts` for unit tests and `*.it.spec.ts` for integration tests. You'd use the following for your configPath setting: 88 | 89 | ```json 90 | { 91 | "jestrunner.configPath": { 92 | "**/*.it.spec.ts": "./jest.it.config.js", 93 | "**/*.spec.ts": "./jest.unit.config.js" 94 | } 95 | } 96 | ``` 97 | 98 | Note the order we've specified the globs in this example. Because our naming convention has a little overlap, we need to specify the more narrow glob first because jestrunner will return the config path of the first matching glob. With the above order, we make certain that `jest.it.config.js` will be used for any file ending with `.it.spec.ts` and `jest.unit.config.js` will be used for files that only end in `*.spec.ts` (without `.it.`). If we had reversed the order, `jest.unit.config.js` would be used for both `*.it.spec.ts` and `*.spec.ts` endings the glob matches both. 99 | 100 | By default, the config path is relative to `{jestrunner.projectPath}` if configured, otherwise the workspace root. 101 | 102 | To find the nearest config matching the result from the `jestrunner.configPath` map, set `"jestrunner.useNearestConfig": true`. When `true`, vscode-jest-runner will search up the through directories from the target until it finds the matching file. For instance, running tests in `~/dev/my-repo/packages/project1/__test__/integration/ship-it.it.spec.ts` will use `~/dev/my-repo/packages/project1/jest.it.config.js` rather than the config in the monorepo's root. 103 | 104 | ## Shortcuts 105 | 106 | Command Pallette -> Preferences: Open Keyboard Shortcuts (JSON) 107 | the json config file will open 108 | add this: 109 | 110 | ```json 111 | { 112 | "key": "alt+1", 113 | "command": "extension.runJest" 114 | }, 115 | { 116 | "key": "alt+2", 117 | "command": "extension.debugJest" 118 | }, 119 | { 120 | "key": "alt+3", 121 | "command": "extension.watchJest" 122 | }, 123 | { 124 | "key": "alt+4", 125 | "command": "extension.runPrevJest" 126 | } 127 | ``` 128 | 129 | ## Want to start contributing features? 130 | 131 | [Check some open topics get you started](https://github.com/firsttris/vscode-jest-runner/issues) 132 | 133 | ### Steps to run Extension in development mode 134 | 135 | - Clone Repo 136 | - npm install 137 | - Go to Menu "Run" => "Start Debugging" 138 | 139 | Another vscode instance will open with the just compiled extension installed. 140 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-jest-runner", 3 | "displayName": "Jest Runner", 4 | "description": "Simple way to run or debug a single (or multiple) tests from context-menu", 5 | "version": "0.4.84", 6 | "publisher": "firsttris", 7 | "author": "Tristan Teufel", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/firsttris/vscode-jest-runner.git" 11 | }, 12 | "license": "MIT", 13 | "icon": "public/icon.png", 14 | "engines": { 15 | "vscode": "^1.83.0" 16 | }, 17 | "categories": [ 18 | "Other" 19 | ], 20 | "activationEvents": [ 21 | "*" 22 | ], 23 | "main": "./dist/extension", 24 | "contributes": { 25 | "configuration": [ 26 | { 27 | "title": "Jest-Runner Config", 28 | "properties": { 29 | "jestrunner.configPath": { 30 | "oneOf": [ 31 | { 32 | "type": "string", 33 | "description": "Jest config path (relative to `${workspaceFolder}` e.g. jest-config.json)" 34 | }, 35 | { 36 | "type": "object", 37 | "scope": "resource", 38 | "description": "A mapping of file globs to jest config paths. Useful when you need to run different jest configurations based on the file naming convention.", 39 | "patternProperties": { 40 | ".*": { 41 | "type": "string", 42 | "description": "The target jest config path for the matched file glob." 43 | } 44 | }, 45 | "additionalProperties": false 46 | } 47 | ], 48 | "default": "", 49 | "scope": "window" 50 | }, 51 | "jestrunner.useNearestConfig": { 52 | "type": "boolean", 53 | "default": false, 54 | "description": "If jestrunner.configPath is set and this is true, the nearest matching jest config in the path hierarchy will be used instead of the root path.", 55 | "scope": "window" 56 | }, 57 | "jestrunner.jestPath": { 58 | "type": "string", 59 | "default": "", 60 | "description": "Absolute path to jest bin file (e.g. /usr/lib/node_modules/jest/bin/jest.js)", 61 | "scope": "window" 62 | }, 63 | "jestrunner.projectPath": { 64 | "type": "string", 65 | "default": "", 66 | "description": "Absolute path to project directory (e.g. /home/me/project/sub-folder)", 67 | "scope": "window" 68 | }, 69 | "jestrunner.debugOptions": { 70 | "type": "object", 71 | "default": {}, 72 | "description": "Add or overwrite vscode debug configurations (only in debug mode) (e.g. { \"args\": [\"--no-cache\"] })", 73 | "scope": "window" 74 | }, 75 | "jestrunner.runOptions": { 76 | "type": "array", 77 | "default": [], 78 | "items": { 79 | "type": "string", 80 | "description": "CLI Option e.g. --coverage" 81 | }, 82 | "description": "Add CLI Options to the Jest Command e.g. https://jestjs.io/docs/en/cli", 83 | "scope": "window" 84 | }, 85 | "jestrunner.jestCommand": { 86 | "type": "string", 87 | "default": "", 88 | "description": "Define an alternative Jest command (e.g. for Create React App and similar abstractions)", 89 | "scope": "window" 90 | }, 91 | "jestrunner.disableCodeLens": { 92 | "type": "boolean", 93 | "default": false, 94 | "description": "Disable CodeLens feature", 95 | "scope": "window" 96 | }, 97 | "jestrunner.runInExternalNativeTerminal": { 98 | "type": "boolean", 99 | "default": false, 100 | "description": "Run jest runner in external native terminal. Disabled on debug mode", 101 | "scope": "window" 102 | }, 103 | "jestrunner.codeLens": { 104 | "type": "array", 105 | "default": [ 106 | "run", 107 | "debug" 108 | ], 109 | "description": "Enable desired codeLens, possible value : 'run', 'debug', 'watch', 'coverage', 'current-test-coverage'. Defaults to ['run', 'debug'] ", 110 | "items": { 111 | "type": "string", 112 | "description": "Either 'run', 'debug', 'watch', 'coverage', 'current-test-coverage'" 113 | }, 114 | "scope": "window" 115 | }, 116 | "jestrunner.codeLensSelector": { 117 | "type": "string", 118 | "default": "**/*.{test,spec}.{js,jsx,ts,tsx}", 119 | "description": "CodeLens will be shown on files matching this pattern" 120 | }, 121 | "jestrunner.enableYarnPnpSupport": { 122 | "type": "boolean", 123 | "default": false, 124 | "description": "Enable if you are using Yarn 2 with Plug'n'Play", 125 | "scope": "window" 126 | }, 127 | "jestrunner.checkRelativePathForJest": { 128 | "type": "boolean", 129 | "default": true, 130 | "description": "When `false`, when looking for the nearest resolution for Jest, only check for package.json files, rather than the `node_modules` folder.", 131 | "scope": "window" 132 | }, 133 | "jestrunner.changeDirectoryToWorkspaceRoot": { 134 | "type": "boolean", 135 | "default": true, 136 | "description": "Changes directory before execution. The fallback order is:\n1. `jestrunner.projectPath`\n2. the nearest `package.json`\n3. `${workspaceFolder}`", 137 | "scope": "window" 138 | }, 139 | "jestrunner.preserveEditorFocus": { 140 | "type": "boolean", 141 | "default": false, 142 | "description": "Preserve focus on editor when running tests", 143 | "scope": "window" 144 | }, 145 | "jestrunner.yarnPnpCommand": { 146 | "type": "string", 147 | "default": "yarn-*.*js", 148 | "description": "Command for debugging with Plug'n'Play", 149 | "scope": "window" 150 | }, 151 | "jestrunner.include": { 152 | "type": "array", 153 | "default": [], 154 | "description": "File globs to include", 155 | "scope": "window" 156 | }, 157 | "jestrunner.exclude": { 158 | "type": "array", 159 | "default": [], 160 | "description": "File globs to exclude", 161 | "scope": "window" 162 | } 163 | } 164 | } 165 | ], 166 | "commands": [ 167 | { 168 | "command": "extension.runJest", 169 | "title": "Run Jest" 170 | }, 171 | { 172 | "command": "extension.runJestPath", 173 | "title": "Run Jest on Path" 174 | }, 175 | { 176 | "command": "extension.runJestCoverage", 177 | "title": "Run Jest and generate Coverage" 178 | }, 179 | { 180 | "command": "extension.runJestAndUpdateSnapshots", 181 | "title": "Run Jest and update Snapshots" 182 | }, 183 | { 184 | "command": "extension.runPrevJest", 185 | "title": "Run Jest - Run Previous Test" 186 | }, 187 | { 188 | "command": "extension.runJestFile", 189 | "title": "Run Jest on File" 190 | }, 191 | { 192 | "command": "extension.debugJest", 193 | "title": "Debug Jest" 194 | }, 195 | { 196 | "command": "extension.debugJestPath", 197 | "title": "Debug Jest on Path" 198 | }, 199 | { 200 | "command": "extension.runJestFileWithCoverage", 201 | "title": "Run Jest File and generate Coverage" 202 | }, 203 | { 204 | "command": "extension.runJestFileWithWatchMode", 205 | "title": "Run Jest File in Watch Mode" 206 | }, 207 | { 208 | "command": "extension.watchJest", 209 | "title": "Run Jest --watch" 210 | } 211 | ], 212 | "menus": { 213 | "editor/context": [ 214 | { 215 | "command": "extension.runJest", 216 | "group": "02_jest" 217 | }, 218 | { 219 | "command": "extension.debugJest", 220 | "group": "02_jest" 221 | } 222 | ], 223 | "explorer/context": [ 224 | { 225 | "command": "extension.runJestPath", 226 | "when": "explorerResourceIsFolder || resourceFilename =~ /.*\\.(spec|test)\\.(js|jsx|ts|tsx)$/", 227 | "group": "02_jest@1" 228 | }, 229 | { 230 | "command": "extension.debugJestPath", 231 | "when": "explorerResourceIsFolder || resourceFilename =~ /.*\\.(spec|test)\\.(js|jsx|ts|tsx)$/", 232 | "group": "02_jest@2" 233 | } 234 | ] 235 | } 236 | }, 237 | "scripts": { 238 | "vscode:prepublish": "webpack --mode production", 239 | "build": "webpack --mode development", 240 | "watch": "webpack --mode development --watch", 241 | "test": "jest .src/test", 242 | "vsce:publish": "vsce publish patch -m '%s [skip ci]' && git push", 243 | "ovsx:publish": "ovsx publish", 244 | "publish": "npm run vsce:publish && npm run ovsx:publish", 245 | "eslint:fix": "eslint --cache --fix", 246 | "prettier": "prettier --write" 247 | }, 248 | "devDependencies": { 249 | "@types/jest": "^29.5.5", 250 | "@types/node": "^20.8.6", 251 | "@types/vscode": "^1.83.0", 252 | "@typescript-eslint/eslint-plugin": "^6.7.5", 253 | "@typescript-eslint/parser": "^6.7.5", 254 | "@vscode/vsce": "^2.21.1", 255 | "eslint": "^8.51.0", 256 | "eslint-config-prettier": "^9.0.0", 257 | "eslint-plugin-prettier": "^5.0.1", 258 | "jest": "^29.7.0", 259 | "ovsx": "^0.8.3", 260 | "prettier": "^3.0.3", 261 | "ts-jest": "^29.1.1", 262 | "ts-loader": "^9.5.0", 263 | "typescript": "^5.2.2", 264 | "webpack": "^5.89.0", 265 | "webpack-cli": "^5.1.4" 266 | }, 267 | "dependencies": { 268 | "fast-glob": "^3.3.3", 269 | "jest-editor-support": "^31.1.2", 270 | "micromatch": "^4.0.8" 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/jestRunner.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as fs from 'fs'; 3 | 4 | import { JestRunnerConfig } from './jestRunnerConfig'; 5 | import { parse } from './parser'; 6 | import { 7 | escapeRegExp, 8 | escapeRegExpForPath, 9 | escapeSingleQuotes, 10 | findFullTestName, 11 | getFileName, 12 | getDirName, 13 | normalizePath, 14 | pushMany, 15 | quote, 16 | unquote, 17 | updateTestNameIfUsingProperties, 18 | } from './util'; 19 | 20 | interface DebugCommand { 21 | documentUri: vscode.Uri; 22 | config: vscode.DebugConfiguration; 23 | } 24 | 25 | export class JestRunner { 26 | private previousCommand: string | DebugCommand; 27 | 28 | private terminal: vscode.Terminal; 29 | 30 | // support for running in a native external terminal 31 | // force runTerminalCommand to push to a queue and run in a native external 32 | // terminal after all commands been pushed 33 | private openNativeTerminal: boolean; 34 | private commands: string[] = []; 35 | 36 | constructor(private readonly config: JestRunnerConfig) { 37 | this.setup(); 38 | this.openNativeTerminal = config.isRunInExternalNativeTerminal; 39 | } 40 | 41 | // 42 | // public methods 43 | // 44 | 45 | public async runTestsOnPath(path: string): Promise { 46 | const command = this.buildJestCommand(path); 47 | 48 | this.previousCommand = command; 49 | 50 | await this.goToCwd(); 51 | await this.runTerminalCommand(command); 52 | 53 | await this.runExternalNativeTerminalCommand(this.commands); 54 | } 55 | 56 | public async runCurrentTest( 57 | argument?: Record | string, 58 | options?: string[], 59 | collectCoverageFromCurrentFile?: boolean, 60 | ): Promise { 61 | const currentTestName = typeof argument === 'string' ? argument : undefined; 62 | const editor = vscode.window.activeTextEditor; 63 | if (!editor) { 64 | return; 65 | } 66 | 67 | await editor.document.save(); 68 | 69 | const filePath = editor.document.fileName; 70 | 71 | const finalOptions = options; 72 | if (collectCoverageFromCurrentFile) { 73 | const targetFileDir = getDirName(filePath); 74 | const targetFileName = getFileName(filePath).replace(/\.(test|spec)\./, '.'); 75 | 76 | // if a file does not exist with the same name as the test file but without the test/spec part 77 | // use test file's directory for coverage target 78 | const coverageTarget = fs.existsSync(`${targetFileDir}/${targetFileName}`) 79 | ? `**/${targetFileName}` 80 | : `**/${getFileName(targetFileDir)}/**`; 81 | 82 | finalOptions.push('--collectCoverageFrom'); 83 | finalOptions.push(quote(coverageTarget)); 84 | } 85 | 86 | const testName = currentTestName || this.findCurrentTestName(editor); 87 | const resolvedTestName = updateTestNameIfUsingProperties(testName); 88 | const command = this.buildJestCommand(filePath, resolvedTestName, finalOptions); 89 | 90 | this.previousCommand = command; 91 | 92 | await this.goToCwd(); 93 | await this.runTerminalCommand(command); 94 | 95 | await this.runExternalNativeTerminalCommand(this.commands); 96 | } 97 | 98 | public async runCurrentFile(options?: string[]): Promise { 99 | const editor = vscode.window.activeTextEditor; 100 | if (!editor) { 101 | return; 102 | } 103 | 104 | await editor.document.save(); 105 | 106 | const filePath = editor.document.fileName; 107 | const command = this.buildJestCommand(filePath, undefined, options); 108 | 109 | this.previousCommand = command; 110 | 111 | await this.goToCwd(); 112 | await this.runTerminalCommand(command); 113 | 114 | await this.runExternalNativeTerminalCommand(this.commands); 115 | } 116 | 117 | public async runPreviousTest(): Promise { 118 | const editor = vscode.window.activeTextEditor; 119 | if (!editor) { 120 | return; 121 | } 122 | 123 | await editor.document.save(); 124 | 125 | if (typeof this.previousCommand === 'string') { 126 | await this.goToCwd(); 127 | await this.runTerminalCommand(this.previousCommand); 128 | } else { 129 | await this.executeDebugCommand(this.previousCommand); 130 | } 131 | 132 | await this.runExternalNativeTerminalCommand(this.commands); 133 | } 134 | 135 | public async debugTestsOnPath(path: string): Promise { 136 | const debugConfig = this.getDebugConfig(path); 137 | 138 | await this.goToCwd(); 139 | await this.executeDebugCommand({ 140 | config: debugConfig, 141 | documentUri: vscode.Uri.file(path), 142 | }); 143 | 144 | await this.runExternalNativeTerminalCommand(this.commands); 145 | } 146 | 147 | public async debugCurrentTest(currentTestName?: string): Promise { 148 | const editor = vscode.window.activeTextEditor; 149 | if (!editor) { 150 | return; 151 | } 152 | 153 | await editor.document.save(); 154 | 155 | const filePath = editor.document.fileName; 156 | const testName = currentTestName || this.findCurrentTestName(editor); 157 | const resolvedTestName = updateTestNameIfUsingProperties(testName); 158 | const debugConfig = this.getDebugConfig(filePath, resolvedTestName); 159 | 160 | await this.goToCwd(); 161 | await this.executeDebugCommand({ 162 | config: debugConfig, 163 | documentUri: editor.document.uri, 164 | }); 165 | 166 | await this.runExternalNativeTerminalCommand(this.commands); 167 | } 168 | 169 | // 170 | // private methods 171 | // 172 | 173 | private async executeDebugCommand(debugCommand: DebugCommand) { 174 | // prevent open of external terminal when debug command is executed 175 | this.openNativeTerminal = false; 176 | 177 | for (const command of this.commands) { 178 | await this.runTerminalCommand(command); 179 | } 180 | this.commands = []; 181 | 182 | vscode.debug.startDebugging(undefined, debugCommand.config); 183 | 184 | this.previousCommand = debugCommand; 185 | } 186 | 187 | private getDebugConfig(filePath: string, currentTestName?: string): vscode.DebugConfiguration { 188 | const config: vscode.DebugConfiguration = { 189 | console: 'integratedTerminal', 190 | internalConsoleOptions: 'neverOpen', 191 | name: 'Debug Jest Tests', 192 | program: this.config.jestBinPath, 193 | request: 'launch', 194 | type: 'node', 195 | cwd: this.config.cwd, 196 | ...this.config.debugOptions, 197 | }; 198 | 199 | config.args = config.args ? config.args.slice() : []; 200 | 201 | if (this.config.isYarnPnpSupportEnabled) { 202 | config.args = ['jest']; 203 | config.program = `.yarn/releases/${this.config.getYarnPnpCommand}`; 204 | } 205 | 206 | const standardArgs = this.buildJestArgs(filePath, currentTestName, false); 207 | pushMany(config.args, standardArgs); 208 | config.args.push('--runInBand'); 209 | 210 | return config; 211 | } 212 | 213 | private findCurrentTestName(editor: vscode.TextEditor): string | undefined { 214 | // from selection 215 | const { selection, document } = editor; 216 | if (!selection.isEmpty) { 217 | return unquote(document.getText(selection)); 218 | } 219 | 220 | const selectedLine = selection.active.line + 1; 221 | const filePath = editor.document.fileName; 222 | const testFile = parse(filePath); 223 | 224 | const fullTestName = findFullTestName(selectedLine, testFile.root.children); 225 | return fullTestName ? escapeRegExp(fullTestName) : undefined; 226 | } 227 | 228 | private buildJestCommand(filePath: string, testName?: string, options?: string[]): string { 229 | const args = this.buildJestArgs(filePath, testName, true, options); 230 | return `${this.config.jestCommand} ${args.join(' ')}`; 231 | } 232 | 233 | private buildJestArgs(filePath: string, testName: string, withQuotes: boolean, options: string[] = []): string[] { 234 | const args: string[] = []; 235 | const quoter = withQuotes ? quote : (str) => str; 236 | 237 | args.push(quoter(escapeRegExpForPath(normalizePath(filePath)))); 238 | 239 | const jestConfigPath = this.config.getJestConfigPath(filePath); 240 | if (jestConfigPath) { 241 | args.push('-c'); 242 | args.push(quoter(normalizePath(jestConfigPath))); 243 | } 244 | 245 | if (testName) { 246 | args.push('-t'); 247 | args.push(quoter(escapeSingleQuotes(testName))); 248 | } 249 | 250 | const setOptions = new Set(options); 251 | 252 | if (this.config.runOptions) { 253 | this.config.runOptions.forEach((option) => setOptions.add(option)); 254 | } 255 | 256 | args.push(...setOptions); 257 | 258 | return args; 259 | } 260 | 261 | private async goToCwd() { 262 | const command = `cd ${quote(this.config.cwd)}`; 263 | if (this.config.changeDirectoryToWorkspaceRoot) { 264 | await this.runTerminalCommand(command); 265 | } 266 | } 267 | 268 | private buildNativeTerminalCommand(toRun: string): string { 269 | const command = `ttab -t 'jest-runner' "${toRun}"`; 270 | return command; 271 | } 272 | 273 | private async runExternalNativeTerminalCommand(commands: string[]): Promise { 274 | if (!this.openNativeTerminal) { 275 | this.commands = []; 276 | return; 277 | } 278 | 279 | const command: string = commands.join('; '); 280 | const externalCommand: string = this.buildNativeTerminalCommand(command); 281 | this.commands = []; 282 | 283 | if (!this.terminal) { 284 | this.terminal = vscode.window.createTerminal('jest'); 285 | } 286 | 287 | this.terminal.show(this.config.preserveEditorFocus); 288 | await vscode.commands.executeCommand('workbench.action.terminal.clear'); 289 | this.terminal.sendText(externalCommand); 290 | } 291 | 292 | private async runTerminalCommand(command: string) { 293 | if (this.openNativeTerminal) { 294 | this.commands.push(command); 295 | return; 296 | } 297 | 298 | if (!this.terminal) { 299 | this.terminal = vscode.window.createTerminal('jest'); 300 | } 301 | this.terminal.show(this.config.preserveEditorFocus); 302 | await vscode.commands.executeCommand('workbench.action.terminal.clear'); 303 | this.terminal.sendText(command); 304 | } 305 | 306 | private setup() { 307 | vscode.window.onDidCloseTerminal((closedTerminal: vscode.Terminal) => { 308 | if (this.terminal === closedTerminal) { 309 | this.terminal = null; 310 | } 311 | }); 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /src/test/jestRunnerConfig.test.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { JestRunnerConfig } from '../jestRunnerConfig'; 3 | import { Document, TextEditor, Uri, WorkspaceConfiguration, WorkspaceFolder } from './__mocks__/vscode'; 4 | import { isWindows, normalizePath } from '../util'; 5 | import * as fs from 'fs'; 6 | import * as path from 'path'; 7 | 8 | const describes = { 9 | windows: isWindows() ? describe : describe.skip, 10 | linux: ['linux', 'darwin'].includes(process.platform) ? describe : describe.skip, 11 | }; 12 | 13 | const its = { 14 | windows: isWindows() ? it : it.skip, 15 | linux: ['linux', 'darwin'].includes(process.platform) ? it : it.skip, 16 | }; 17 | 18 | describe('JestRunnerConfig', () => { 19 | describes.windows('Windows style paths', () => { 20 | let jestRunnerConfig: JestRunnerConfig; 21 | beforeEach(() => { 22 | jestRunnerConfig = new JestRunnerConfig(); 23 | jest 24 | .spyOn(vscode.workspace, 'getWorkspaceFolder') 25 | .mockReturnValue(new WorkspaceFolder(new Uri('C:\\project') as any) as any); 26 | }); 27 | 28 | it.each([ 29 | ['absolute path (with \\)', 'C:\\project\\jestProject'], 30 | ['absolute path (with /)', 'C:/project/jestProject'], 31 | ['relative path', './jestProject'], 32 | ])('%s', (_testName, projectPath) => { 33 | jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue( 34 | new WorkspaceConfiguration({ 35 | 'jestrunner.projectPath': projectPath, 36 | }), 37 | ); 38 | 39 | expect(jestRunnerConfig.cwd).toBe('C:\\project\\jestProject'); 40 | }); 41 | }); 42 | 43 | describes.linux('Linux style paths', () => { 44 | let jestRunnerConfig: JestRunnerConfig; 45 | 46 | beforeEach(() => { 47 | jestRunnerConfig = new JestRunnerConfig(); 48 | jest 49 | .spyOn(vscode.workspace, 'getWorkspaceFolder') 50 | .mockReturnValue(new WorkspaceFolder(new Uri('/home/user/project') as any) as any); 51 | }); 52 | 53 | it.each([ 54 | ['absolute path', '/home/user/project/jestProject'], 55 | ['relative path', './jestProject'], 56 | ])('%s', (_testName, projectPath) => { 57 | jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue( 58 | new WorkspaceConfiguration({ 59 | 'jestrunner.projectPath': projectPath, 60 | }), 61 | ); 62 | 63 | expect(jestRunnerConfig.cwd).toBe('/home/user/project/jestProject'); 64 | }); 65 | }); 66 | 67 | describe('getJestConfigPath', () => { 68 | describe('configPath is a string', () => { 69 | const scenarios: Array< 70 | [ 71 | os: 'windows' | 'linux', 72 | name: string, 73 | behavior: string, 74 | workspacePath: string, 75 | projectPath: string | undefined, 76 | configPath: string, 77 | targetPath: string, 78 | expectedPath: string, 79 | ] 80 | > = [ 81 | [ 82 | 'linux', 83 | 'configPath is an absolute path', 84 | 'returned path is only the specified config path', 85 | '/home/user/workspace', 86 | './jestProject', 87 | '/home/user/notWorkspace/notJestProject/jest.config.js', 88 | '/home/user/workspace/jestProject/src/index.test.js', 89 | '/home/user/notWorkspace/notJestProject/jest.config.js', 90 | ], 91 | [ 92 | 'linux', 93 | 'configPath is a relative path, project path is set', 94 | 'returned path is resolved against workspace and project path', 95 | '/home/user/workspace', 96 | './jestProject', 97 | './jest.config.js', 98 | '/home/user/workspace/jestProject/src/index.test.js', 99 | '/home/user/workspace/jestProject/jest.config.js', 100 | ], 101 | [ 102 | 'linux', 103 | 'configPath is a relative path, projectPath is not set', 104 | 'returned path is resolved against workspace path', 105 | '/home/user/workspace', 106 | undefined, 107 | './jest.config.js', 108 | '/home/user/workspace/jestProject/src/index.test.js', 109 | '/home/user/workspace/jest.config.js', 110 | ], 111 | 112 | [ 113 | 'windows', 114 | 'configPath is an absolute path (with \\)', 115 | 'returned path is only the specified config path', 116 | 'C:/workspace', 117 | './jestProject', 118 | 'C:\\notWorkspace\\notJestProject\\jest.config.js', 119 | 'C:/workspace/jestProject/src/index.test.js', 120 | 'C:/notWorkspace/notJestProject/jest.config.js', 121 | ], 122 | [ 123 | 'windows', 124 | 'configPath is an absolute path (with /)', 125 | 'returned path is only the (normalized) specified config path', 126 | 'C:/workspace', 127 | './jestProject', 128 | 'C:/notWorkspace/notJestProject/jest.config.js', 129 | 'C:/workspace/jestProject/src/index.test.js', 130 | 'C:/notWorkspace/notJestProject/jest.config.js', 131 | ], 132 | [ 133 | 'windows', 134 | 'configPath is a relative path, project path is set', 135 | 'returned path is resolved against workspace and project path', 136 | 'C:/workspace', 137 | './jestProject', 138 | './jest.config.js', 139 | 'C:/workspace/jestProject/src/index.test.js', 140 | 'C:/workspace/jestProject/jest.config.js', 141 | ], 142 | [ 143 | 'windows', 144 | 'configPath is a relative path, projectPath is not set', 145 | 'returned path is resolved against workspace path', 146 | 'C:/workspace', 147 | undefined, 148 | './jest.config.js', 149 | 'C:/workspace/jestProject/src/index.test.js', 150 | 'C:/workspace/jest.config.js', 151 | ], 152 | ]; 153 | describe.each(scenarios)( 154 | '%s: %s', 155 | ( 156 | _os, 157 | _name, 158 | behavior, 159 | workspacePath, 160 | projectPath, 161 | configPath, 162 | targetPath, 163 | expectedPath, 164 | useNearestConfig = undefined, 165 | ) => { 166 | let jestRunnerConfig: JestRunnerConfig; 167 | 168 | beforeEach(() => { 169 | jestRunnerConfig = new JestRunnerConfig(); 170 | jest 171 | .spyOn(vscode.workspace, 'getWorkspaceFolder') 172 | .mockReturnValue(new WorkspaceFolder(new Uri(workspacePath) as any) as any); 173 | }); 174 | 175 | its[_os](behavior, async () => { 176 | jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue( 177 | new WorkspaceConfiguration({ 178 | 'jestrunner.projectPath': projectPath, 179 | 'jestrunner.configPath': configPath, 180 | 'jestrunner.useNearestConfig': useNearestConfig, 181 | }), 182 | ); 183 | 184 | expect(jestRunnerConfig.getJestConfigPath(targetPath)).toBe(expectedPath); 185 | }); 186 | }, 187 | ); 188 | }); 189 | describe('configPath is a glob map', () => { 190 | describe('there is a matching glob', () => { 191 | const scenarios: Array< 192 | [ 193 | os: 'windows' | 'linux', 194 | name: string, 195 | behavior: string, 196 | workspacePath: string, 197 | projectPath: string | undefined, 198 | configPath: Record, 199 | targetPath: string, 200 | expectedPath: string, 201 | useNearestConfig?: boolean, 202 | ] 203 | > = [ 204 | [ 205 | 'linux', 206 | 'matched glob specifies an absolute path', 207 | 'returned path is only the specified config path', 208 | '/home/user/workspace', 209 | './jestProject', 210 | { '**/*.test.js': '/home/user/workspace/jestProject/jest.config.js' }, 211 | '/home/user/workspace/jestProject/src/index.test.js', 212 | '/home/user/workspace/jestProject/jest.config.js', 213 | ], 214 | [ 215 | 'linux', 216 | 'matched glob specifies a relative path', 217 | 'returned path is resolved against workspace and project path', 218 | '/home/user/workspace', 219 | './jestProject', 220 | { '**/*.test.js': './jest.config.js' }, 221 | '/home/user/workspace/jestProject/src/index.test.js', 222 | '/home/user/workspace/jestProject/jest.config.js', 223 | ], 224 | [ 225 | 'linux', 226 | 'matched glob specifies a relative path, projectPath is not set', 227 | 'returned path is resolved against workspace path', 228 | '/home/user/workspace', 229 | undefined, 230 | { '**/*.test.js': './jest.config.js' }, 231 | '/home/user/workspace/jestProject/src/index.test.js', 232 | '/home/user/workspace/jest.config.js', 233 | ], 234 | [ 235 | 'linux', 236 | 'matched glob specifies a relative path, useNearestConfig is true', 237 | 'returned path is the nearest config in project', 238 | '/home/user/workspace', 239 | undefined, 240 | { '**/*.test.js': './jest.config.js' }, 241 | '/home/user/workspace/jestProject/src/index.test.js', 242 | '/home/user/workspace/jestProject/jest.config.js', 243 | true, 244 | ], 245 | [ 246 | 'linux', 247 | 'first matched glob takes precedence, relative path', 248 | 'returned path is resolved against workspace and project path', 249 | '/home/user/workspace', 250 | './jestProject', 251 | { 252 | '**/*.test.js': './jest.config.js', 253 | '**/*.spec.js': './jest.unit-config.js', 254 | '**/*.it.spec.js': './jest.it-config.js', 255 | }, 256 | '/home/user/workspace/jestProject/src/index.it.spec.js', 257 | '/home/user/workspace/jestProject/jest.unit-config.js', 258 | ], 259 | [ 260 | 'linux', 261 | 'first matched glob takes precedence, relative path, useNearestConfig is true', 262 | 'returned path is the nearest config in project', 263 | '/home/user/workspace', 264 | './aDifferentProject', 265 | { 266 | '**/*.test.js': './jest.config.js', 267 | '**/*.spec.js': './jest.unit-config.js', 268 | '**/*.it.spec.js': './jest.it-config.js', 269 | }, 270 | '/home/user/workspace/jestProject/src/index.it.spec.js', 271 | '/home/user/workspace/jestProject/jest.unit-config.js', 272 | true, 273 | ], 274 | [ 275 | 'linux', 276 | 'first matched glob takes precedence, absolute path', 277 | 'returned path is only the specified config path', 278 | '/home/user/workspace', 279 | './jestProject', 280 | { 281 | '**/*.test.js': '/home/user/notWorkspace/notJestProject/jest.config.js', 282 | '**/*.spec.js': '/home/user/notWorkspace/notJestProject/jest.unit-config.js', 283 | '**/*.it.spec.js': '/home/user/notWorkspace/notJestProject/jest.it-config.js', 284 | }, 285 | '/home/user/workspace/jestProject/src/index.it.spec.js', 286 | '/home/user/notWorkspace/notJestProject/jest.unit-config.js', 287 | ], 288 | [ 289 | 'linux', 290 | 'first matched glob takes precedence, useNearestConfig falls back to absolute path', 291 | 'returned path is only the specified config path', 292 | '/home/user/workspace', 293 | './jestProject', 294 | { 295 | '**/*.test.js': '/home/user/notWorkspace/notJestProject/jest.config.js', 296 | '**/*.spec.js': '/home/user/notWorkspace/notJestProject/jest.unit-config.js', 297 | '**/*.it.spec.js': '/home/user/notWorkspace/notJestProject/jest.it-config.js', 298 | }, 299 | '/home/user/workspace/jestProject/src/index.it.spec.js', 300 | '/home/user/notWorkspace/notJestProject/jest.unit-config.js', 301 | true, 302 | ], 303 | // windows 304 | [ 305 | 'windows', 306 | 'matched glob specifies an absolute path (with \\)', 307 | 'returned path is only the specified (normalized) config path', 308 | 'C:/workspace', 309 | './jestProject', 310 | { '**/*.test.js': 'C:\\notWorkspace\\notJestProject\\jest.config.js' }, 311 | 'C:/workspace/jestProject/src/index.test.js', 312 | 'C:/notWorkspace/notJestProject/jest.config.js', 313 | ], 314 | [ 315 | 'windows', 316 | 'matched glob specifies an absolute path (with /)', 317 | 'returned path is only the specified (normalized) config path', 318 | 'C:/workspace', 319 | './jestProject', 320 | { '**/*.test.js': 'C:/notWorkspace/notJestProject/jest.config.js' }, 321 | 'C:/workspace/jestProject/src/index.test.js', 322 | 'C:/notWorkspace/notJestProject/jest.config.js', 323 | ], 324 | [ 325 | 'windows', 326 | 'matched glob specifies a relative path, projectPath is set', 327 | 'returned (normalized) path is resolved against workspace and project path', 328 | 'C:/workspace', 329 | './jestProject', 330 | { '**/*.test.js': './jest.config.js' }, 331 | 'C:/workspace/jestProject/src/index.test.js', 332 | 'C:/workspace/jestProject/jest.config.js', 333 | ], 334 | [ 335 | 'windows', 336 | 'matched glob specifies a relative path, projectPath is not set', 337 | 'returned (normalized) path is resolved against workspace path', 338 | 'C:/workspace', 339 | undefined, 340 | { '**/*.test.js': './jest.config.js' }, 341 | 'C:/workspace/jestProject/src/index.test.js', 342 | 'C:/workspace/jest.config.js', 343 | ], 344 | [ 345 | 'windows', 346 | 'matched glob specifies a relative path, useNearestConfig is true', 347 | 'returned path is the nearest config in project', 348 | 'C:/workspace', 349 | undefined, 350 | { '**/*.test.js': './jest.config.js' }, 351 | 'C:/workspace/jestProject/src/index.test.js', 352 | 'C:/workspace/jestProject/jest.config.js', 353 | true, 354 | ], 355 | [ 356 | 'windows', 357 | 'first matched glob takes precedence, relative path', 358 | 'returned(normalized) path is resolved against workspace and project path', 359 | 'C:\\workspace', 360 | './jestProject', 361 | { 362 | '**/*.test.js': './jest.config.js', 363 | '**/*.spec.js': './jest.unit-config.js', 364 | '**/*.it.spec.js': './jest.it-config.js', 365 | }, 366 | 'C:/workspace/jestProject/src/index.it.spec.js', 367 | 'C:/workspace/jestProject/jest.unit-config.js', 368 | ], 369 | [ 370 | 'windows', 371 | 'first matched glob takes precedence, relative path, useNearestConfig is true', 372 | 'returned path is the nearest config in project', 373 | 'C:/workspace', 374 | './aDifferentProject', 375 | { 376 | '**/*.test.js': './jest.config.js', 377 | '**/*.spec.js': './jest.unit-config.js', 378 | '**/*.it.spec.js': './jest.it-config.js', 379 | }, 380 | 'C:/workspace/jestProject/src/index.it.spec.js', 381 | 'C:/workspace/jestProject/jest.unit-config.js', 382 | true, 383 | ], 384 | [ 385 | 'windows', 386 | 'first matched glob takes precedence, absolute path (with \\)', 387 | 'returned (normalized) path is only the (normalized) specified config path', 388 | 'C:/workspace', 389 | './jestProject', 390 | { 391 | '**/*.test.js': 'C:\\notWorkspace\\notJestProject\\jest.config.js', 392 | '**/*.spec.js': 'C:\\notWorkspace\\notJestProject\\jest.unit-config.js', 393 | '**/*.it.spec.js': 'C:\\notWorkspace\\notJestProject\\jest.it-config.js', 394 | }, 395 | 'C:/workspace/jestProject/src/index.it.spec.js', 396 | 'C:/notWorkspace/notJestProject/jest.unit-config.js', 397 | ], 398 | [ 399 | 'windows', 400 | 'first matched glob takes precedence, absolute path (with /)', 401 | 'returned (normalized) path is only the specified config path', 402 | 'C:/workspace', 403 | './jestProject', 404 | { 405 | '**/*.test.js': 'C:/notWorkspace/notJestProject/jest.config.js', 406 | '**/*.spec.js': 'C:/notWorkspace/notJestProject/jest.unit-config.js', 407 | '**/*.it.spec.js': 'C:/notWorkspace/notJestProject/jest.it-config.js', 408 | }, 409 | 'C:/workspace/jestProject/src/index.it.spec.js', 410 | 'C:/notWorkspace/notJestProject/jest.unit-config.js', 411 | ], 412 | [ 413 | 'windows', 414 | 'first matched glob takes precedence, useNearestConfig falls back to absolute path', 415 | 'returned path is only the specified config path', 416 | 'C:/workspace', 417 | './jestProject', 418 | { 419 | '**/*.test.js': '/home/user/notWorkspace/notJestProject/jest.config.js', 420 | '**/*.spec.js': '/home/user/notWorkspace/notJestProject/jest.unit-config.js', 421 | '**/*.it.spec.js': '/home/user/notWorkspace/notJestProject/jest.it-config.js', 422 | }, 423 | 'C:/workspace/jestProject/src/index.it.spec.js', 424 | 'C:/notWorkspace/notJestProject/jest.unit-config.js', 425 | true, 426 | ], 427 | ]; 428 | describe.each(scenarios)( 429 | '%s: %s', 430 | ( 431 | _os, 432 | _name, 433 | behavior, 434 | workspacePath, 435 | projectPath, 436 | configPath, 437 | targetPath, 438 | expectedPath, 439 | useNearestConfig = undefined, 440 | ) => { 441 | let jestRunnerConfig: JestRunnerConfig; 442 | 443 | beforeEach(() => { 444 | jestRunnerConfig = new JestRunnerConfig(); 445 | jest 446 | .spyOn(vscode.workspace, 'getWorkspaceFolder') 447 | .mockReturnValue(new WorkspaceFolder(new Uri(workspacePath) as any) as any); 448 | jest.spyOn(fs, 'statSync').mockImplementation((path): any => { 449 | if (path === targetPath) { 450 | return { isFile: () => true, isDirectory: () => false }; 451 | } 452 | return { isFile: () => false, isDirectory: () => true }; 453 | }); 454 | // Return true if getJestConfigPath is checking the expected path 455 | jest.spyOn(fs, 'existsSync').mockImplementation((filePath) => filePath === expectedPath); 456 | }); 457 | 458 | its[_os](behavior, async () => { 459 | jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue( 460 | new WorkspaceConfiguration({ 461 | 'jestrunner.projectPath': projectPath, 462 | 'jestrunner.configPath': configPath, 463 | 'jestrunner.useNearestConfig': useNearestConfig, 464 | }), 465 | ); 466 | 467 | expect(jestRunnerConfig.getJestConfigPath(targetPath)).toBe(normalizePath(expectedPath)); 468 | }); 469 | }, 470 | ); 471 | }); 472 | 473 | describe('no matching glob', () => { 474 | const scenarios: Array< 475 | [ 476 | os: 'windows' | 'linux', 477 | name: string, 478 | behavior: string, 479 | workspacePath: string, 480 | projectPath: string | undefined, 481 | configPath: Record, 482 | targetPath: string, 483 | foundPath: string, 484 | expectedPath: string, 485 | ] 486 | > = [ 487 | [ 488 | 'linux', 489 | 'projectPath is relative', 490 | 'returns the found jest config path (traversing up from target path)', 491 | '/home/user/workspace', 492 | './jestProject', 493 | { 494 | '**/*.test.js': './jest.config.js', 495 | }, 496 | '/home/user/workspace/jestProject/src/index.unit.spec.js', 497 | '/home/user/workspace/jestProject/jest.config.mjs', 498 | '/home/user/workspace/jestProject/jest.config.mjs', 499 | ], 500 | [ 501 | 'linux', 502 | 'projectPath is not set', 503 | 'returns the found jest config path (traversing up from target path)', 504 | '/home/user/workspace', 505 | undefined, 506 | { 507 | '**/*.test.js': './jest.config.js', 508 | }, 509 | '/home/user/workspace/src/index.unit.spec.js', 510 | '/home/user/workspace/jest.config.mjs', 511 | '/home/user/workspace/jest.config.mjs', 512 | ], 513 | // windows 514 | [ 515 | 'windows', 516 | 'projectPath is relative', 517 | 'returns the (normalized) found jest config (traversing up from target path)', 518 | 'C:\\workspace', 519 | './jestProject', 520 | { 521 | '**/*.test.js': 'C:/notWorkspace/notJestProject/jest.config.js', 522 | }, 523 | 'C:\\workspace\\jestProject\\src\\index.it.spec.js', 524 | 'C:\\workspace\\jestProject\\jest.config.mjs', 525 | 'C:/workspace/jestProject/jest.config.mjs', 526 | ], 527 | [ 528 | 'windows', 529 | 'projectPath is not set', 530 | 'returns the (normalized) found jest config path (traversing up from target path)', 531 | 'C:\\workspace', 532 | undefined, 533 | { 534 | '**/*.test.js': 'C:/notWorkspace/notJestProject/jest.config.js', 535 | }, 536 | 'C:\\workspace\\src\\index.it.spec.js', 537 | 'C:\\workspace\\jest.config.mjs', 538 | 'C:/workspace/jest.config.mjs', 539 | ], 540 | ]; 541 | describe.each(scenarios)( 542 | '%s: %s', 543 | (_os, _name, behavior, workspacePath, projectPath, configPath, targetPath, foundPath, expectedPath) => { 544 | let jestRunnerConfig: JestRunnerConfig; 545 | 546 | beforeEach(() => { 547 | jestRunnerConfig = new JestRunnerConfig(); 548 | jest 549 | .spyOn(vscode.workspace, 'getWorkspaceFolder') 550 | .mockReturnValue(new WorkspaceFolder(new Uri(workspacePath) as any) as any); 551 | jest.spyOn(vscode.window, 'showWarningMessage').mockReturnValue(undefined); 552 | jest.spyOn(fs, 'statSync').mockImplementation((path: string): any => ({ 553 | isDirectory: () => /\.[a-z]{2,4}$/.test(path), 554 | })); 555 | }); 556 | 557 | its[_os](behavior, async () => { 558 | jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue( 559 | new WorkspaceConfiguration({ 560 | 'jestrunner.projectPath': projectPath, 561 | 'jestrunner.configPath': configPath, 562 | }), 563 | ); 564 | jest.spyOn(fs, 'existsSync').mockImplementation((filePath) => filePath === foundPath); 565 | 566 | expect(jestRunnerConfig.getJestConfigPath(targetPath)).toBe(expectedPath); 567 | }); 568 | }, 569 | ); 570 | }); 571 | }); 572 | describe('configPath is not set', () => { 573 | const scenarios: Array< 574 | [ 575 | os: 'windows' | 'linux', 576 | name: string, 577 | behavior: string, 578 | workspacePath: string, 579 | projectPath: string | undefined, 580 | targetPath: string, 581 | expectedPath: string, 582 | ] 583 | > = [ 584 | [ 585 | 'linux', 586 | 'without project path', 587 | 'returns the found jest config path (traversing up from target path)', 588 | '/home/user/workspace', 589 | undefined, 590 | '/home/user/workspace/jestProject/src/index.unit.spec.js', 591 | '/home/user/workspace/jestProject/jest.config.mjs', 592 | ], 593 | [ 594 | 'linux', 595 | 'with projectPath defined', 596 | 'returns the found jest config path (traversing up from target path)', 597 | '/home/user/workspace', 598 | './anotherProject', 599 | '/home/user/workspace/jestProject/src/index.unit.spec.js', 600 | '/home/user/workspace/jestProject/jest.config.mjs', 601 | ], 602 | [ 603 | 'linux', 604 | 'with projectPath defined and no config in project', 605 | 'returns the found jest config path in workspace (traversing up from target path)', 606 | '/home/user/workspace', 607 | './anotherProject', 608 | '/home/user/workspace/anotherProject/src/index.unit.spec.js', 609 | '/home/user/workspace/jest.config.mjs', 610 | ], 611 | [ 612 | 'linux', 613 | 'with no configs found', 614 | 'returns an empty string', 615 | '/home/user/workspace', 616 | './anotherProject', 617 | '/home/user/workspace/anotherProject/src/index.unit.spec.js', 618 | '', 619 | ], 620 | // windows 621 | [ 622 | 'windows', 623 | 'without project path', 624 | 'returns the found jest config path (traversing up from target path)', 625 | 'C:\\workspace', 626 | undefined, 627 | 'C:\\workspace\\jestProject\\src\\index.unit.spec.js', 628 | 'C:\\workspace\\jestProject\\jest.config.mjs', 629 | ], 630 | [ 631 | 'windows', 632 | 'with projectPath defined', 633 | 'returns the found jest config path (traversing up from target path)', 634 | 'C:\\workspace', 635 | './anotherProject', 636 | 'C:\\workspace\\jestProject\\src\\index.unit.spec.js', 637 | 'C:\\workspace\\jestProject\\jest.config.mjs', 638 | ], 639 | [ 640 | 'windows', 641 | 'with projectPath defined and no config in project', 642 | 'returns the found jest config path in workspace (traversing up from target path)', 643 | 'C:\\workspace', 644 | './anotherProject', 645 | 'C:\\workspace\\anotherProject\\src\\index.unit.spec.js', 646 | 'C:\\workspace\\jest.config.mjs', 647 | ], 648 | [ 649 | 'windows', 650 | 'with no configs found', 651 | 'returns an empty string', 652 | 'C:\\workspace', 653 | './anotherProject', 654 | 'C:\\workspace\\anotherProject\\src\\index.unit.spec.js', 655 | '', 656 | ], 657 | ]; 658 | describe.each(scenarios)( 659 | '%s: %s', 660 | (_os, _name, behavior, workspacePath, projectPath, targetPath, expectedPath) => { 661 | let jestRunnerConfig: JestRunnerConfig; 662 | 663 | beforeEach(() => { 664 | jestRunnerConfig = new JestRunnerConfig(); 665 | jest 666 | .spyOn(vscode.workspace, 'getWorkspaceFolder') 667 | .mockReturnValue(new WorkspaceFolder(new Uri(workspacePath) as any) as any); 668 | 669 | jest.spyOn(fs, 'statSync').mockImplementation((path): any => { 670 | if (path === targetPath) { 671 | return { isFile: () => true, isDirectory: () => false }; 672 | } 673 | return { isFile: () => false, isDirectory: () => true }; 674 | }); 675 | // Return true if getJestConfigPath is checking the expected path 676 | jest.spyOn(fs, 'existsSync').mockImplementation((filePath) => filePath === expectedPath); 677 | }); 678 | 679 | its[_os](behavior, async () => { 680 | jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue( 681 | new WorkspaceConfiguration({ 682 | 'jestrunner.projectPath': projectPath, 683 | 'jestrunner.configPath': undefined, 684 | }), 685 | ); 686 | 687 | expect(jestRunnerConfig.getJestConfigPath(targetPath)).toBe(expectedPath); 688 | }); 689 | }, 690 | ); 691 | }); 692 | }); 693 | 694 | describe('currentPackagePath', () => { 695 | const scenarios: Array< 696 | [ 697 | os: 'windows' | 'linux', 698 | name: string, 699 | behavior: string, 700 | workspacePath: string, 701 | openedFilePath: string, 702 | installedPath: string | undefined, 703 | ] 704 | > = [ 705 | [ 706 | 'linux', 707 | 'jest dep installed in same path as the opened file', 708 | 'returns the folder path of the opened file', 709 | '/home/user/workspace', 710 | '/home/user/workspace/jestProject/index.test.js', 711 | '/home/user/workspace/jestProject', 712 | ], 713 | [ 714 | 'linux', 715 | 'jest dep installed in parent path of the opened file', 716 | 'returns the folder path of the parent of the opened file', 717 | '/home/user/workspace', 718 | '/home/user/workspace/jestProject/src/index.test.js', 719 | '/home/user/workspace/jestProject', 720 | ], 721 | [ 722 | 'linux', 723 | 'jest dep installed in an ancestor path of the opened file', 724 | 'returns the folder path of the ancestor of the opened file', 725 | '/home/user/workspace', 726 | '/home/user/workspace/jestProject/deeply/nested/package/src/index.test.js', 727 | '/home/user/workspace/jestProject', 728 | ], 729 | [ 730 | 'linux', 731 | 'jest dep installed in the workspace of the opened file', 732 | "returns the folder path of the opened file's workspace", 733 | '/home/user/workspace', 734 | '/home/user/workspace/jestProject/deeply/nested/package/src/index.test.js', 735 | '/home/user/workspace', 736 | ], 737 | [ 738 | 'linux', 739 | 'jest dep not installed', 740 | 'returns empty string', 741 | '/home/user/workspace', 742 | '/home/user/workspace/jestProject/deeply/nested/package/src/index.test.js', 743 | undefined, 744 | ], 745 | // windows 746 | [ 747 | 'windows', 748 | 'jest dep installed in same path as the opened file', 749 | 'returns the (normalized) folder path of the opened file', 750 | 'C:\\workspace', 751 | 'C:\\workspace\\jestProject\\src\\index.it.spec.js', 752 | 'C:\\workspace\\jestProject\\src', 753 | ], 754 | [ 755 | 'windows', 756 | 'jest dep installed in parent path of the opened file', 757 | 'returns the (normalized) folder path of the parent of the opened file', 758 | 'C:\\workspace', 759 | 'C:\\workspace\\jestProject\\src\\index.it.spec.js', 760 | 'C:\\workspace\\jestProject', 761 | ], 762 | [ 763 | 'windows', 764 | 'jest dep installed in an ancestor path of the opened file', 765 | 'returns the (normalized) folder path of the ancestor of the opened file', 766 | 'C:\\workspace', 767 | 'C:\\workspace\\jestProject\\deeply\\nested\\package\\src\\index.it.spec.js', 768 | 'C:\\workspace\\jestProject', 769 | ], 770 | [ 771 | 'windows', 772 | 'jest dep installed in the workspace of the opened file', 773 | "returns the (normalized) folder path of the opened file's workspace", 774 | 'C:\\workspace', 775 | 'C:\\workspace\\jestProject\\src\\index.it.spec.js', 776 | 'C:\\workspace', 777 | ], 778 | [ 779 | 'windows', 780 | 'jest dep not installed', 781 | 'returns empty string', 782 | 'C:\\workspace', 783 | 'C:\\workspace\\jestProject\\src\\index.it.spec.js', 784 | undefined, 785 | ], 786 | ]; 787 | 788 | describe.each(scenarios)('%s: %s', (_os, _name, behavior, workspacePath, openedFilePath, installedPath) => { 789 | let jestRunnerConfig: JestRunnerConfig; 790 | let packagePath: string; 791 | let modulePath: string; 792 | 793 | beforeEach(() => { 794 | jestRunnerConfig = new JestRunnerConfig(); 795 | packagePath = installedPath ? path.resolve(installedPath, 'package.json') : ''; 796 | modulePath = installedPath ? path.resolve(installedPath, 'node_modules', 'jest') : ''; 797 | jest 798 | .spyOn(vscode.workspace, 'getWorkspaceFolder') 799 | .mockReturnValue(new WorkspaceFolder(new Uri(workspacePath) as any) as any); 800 | jest 801 | .spyOn(vscode.window, 'activeTextEditor', 'get') 802 | .mockReturnValue(new TextEditor(new Document(new Uri(openedFilePath))) as any); 803 | jest.spyOn(fs, 'statSync').mockImplementation((path): any => ({ 804 | isDirectory: () => !openedFilePath.endsWith('.ts'), 805 | })); 806 | jest 807 | .spyOn(fs, 'existsSync') 808 | .mockImplementation((filePath) => filePath === packagePath || filePath === modulePath); 809 | jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue( 810 | new WorkspaceConfiguration({ 811 | 'jestrunner.checkRelativePathForJest': false, 812 | }), 813 | ); 814 | }); 815 | 816 | its[_os](behavior, async () => { 817 | if (installedPath) { 818 | expect(jestRunnerConfig.currentPackagePath).toBe(normalizePath(installedPath)); 819 | } else { 820 | expect(jestRunnerConfig.currentPackagePath).toBe(''); 821 | } 822 | }); 823 | 824 | describe('checkRelativePathForJest is set to true', () => { 825 | beforeEach(() => { 826 | jest.spyOn(vscode.workspace, 'getConfiguration').mockReturnValue( 827 | new WorkspaceConfiguration({ 828 | 'jestrunner.checkRelativePathForJest': true, 829 | }), 830 | ); 831 | }); 832 | 833 | its[_os](behavior, async () => { 834 | if (installedPath) { 835 | expect(jestRunnerConfig.currentPackagePath).toBe(normalizePath(installedPath)); 836 | } else { 837 | expect(jestRunnerConfig.currentPackagePath).toBe(''); 838 | } 839 | }); 840 | }); 841 | }); 842 | }); 843 | describe('findConfigPath', () => { 844 | const scenarios: Array< 845 | [ 846 | os: 'windows' | 'linux', 847 | name: string, 848 | behavior: string, 849 | workspacePath: string, 850 | openedFilePath: string, 851 | configPath: string | undefined, 852 | configFileName: string | undefined, 853 | ] 854 | > = [ 855 | [ 856 | 'linux', 857 | 'jest config located in same path as the opened file', 858 | 'returns the filename path of the found config file', 859 | '/home/user/workspace', 860 | '/home/user/workspace/jestProject/index.test.js', 861 | '/home/user/workspace/jestProject', 862 | 'jest.config.cjs', 863 | ], 864 | [ 865 | 'linux', 866 | 'jest config located in parent path of the opened file', 867 | 'returns the filename path of the found config file', 868 | '/home/user/workspace', 869 | '/home/user/workspace/jestProject/src/index.test.js', 870 | '/home/user/workspace/jestProject', 871 | 'jest.config.json', 872 | ], 873 | [ 874 | 'linux', 875 | 'jest config located in an ancestor path of the opened file', 876 | 'returns the filename path of the found config file', 877 | '/home/user/workspace', 878 | '/home/user/workspace/jestProject/deeply/nested/package/src/index.test.js', 879 | '/home/user/workspace/jestProject', 880 | 'jest.config.js', 881 | ], 882 | [ 883 | 'linux', 884 | 'jest config located in the workspace of the opened file', 885 | 'returns the filename path of the found config file', 886 | '/home/user/workspace', 887 | '/home/user/workspace/jestProject/deeply/nested/package/src/index.test.js', 888 | '/home/user/workspace', 889 | 'jest.config.ts', 890 | ], 891 | [ 892 | 'linux', 893 | 'jest config not located', 894 | 'returns empty string', 895 | '/home/user/workspace', 896 | '/home/user/workspace/jestProject/deeply/nested/package/src/index.test.js', 897 | undefined, 898 | undefined, 899 | ], 900 | // windows 901 | [ 902 | 'windows', 903 | 'jest config located in same path as the opened file', 904 | 'returns the (normalized) folder path of the opened file', 905 | 'C:\\workspace', 906 | 'C:\\workspace\\jestProject\\src\\index.it.spec.js', 907 | 'C:\\workspace\\jestProject\\src', 908 | 'jest.config.cjs', 909 | ], 910 | [ 911 | 'windows', 912 | 'jest config located in parent path of the opened file', 913 | 'returns the (normalized) folder path of the parent of the opened file', 914 | 'C:\\workspace', 915 | 'C:\\workspace\\jestProject\\src\\index.it.spec.js', 916 | 'C:\\workspace\\jestProject', 917 | 'jest.config.json', 918 | ], 919 | [ 920 | 'windows', 921 | 'jest config located in an ancestor path of the opened file', 922 | 'returns the (normalized) folder path of the ancestor of the opened file', 923 | 'C:\\workspace', 924 | 'C:\\workspace\\jestProject\\deeply\\nested\\package\\src\\index.it.spec.js', 925 | 'C:\\workspace\\jestProject', 926 | 'jest.config.js', 927 | ], 928 | [ 929 | 'windows', 930 | 'jest config located in the workspace of the opened file', 931 | "returns the (normalized) folder path of the opened file's workspace", 932 | 'C:\\workspace', 933 | 'C:\\workspace\\jestProject\\src\\index.it.spec.js', 934 | 'C:\\workspace', 935 | 'jest.config.ts', 936 | ], 937 | [ 938 | 'windows', 939 | 'jest config not located', 940 | 'returns empty string', 941 | 'C:\\workspace', 942 | 'C:\\workspace\\jestProject\\src\\index.it.spec.js', 943 | undefined, 944 | undefined, 945 | ], 946 | ]; 947 | 948 | describe('targetPath is not provided', () => { 949 | describe.each(scenarios)( 950 | '%s: %s', 951 | (_os, _name, behavior, workspacePath, openedFilePath, configPath, configFilename) => { 952 | let jestRunnerConfig: JestRunnerConfig; 953 | let configFilePath: string; 954 | let activeTextEditorSpy: jest.SpyInstance; 955 | 956 | beforeEach(() => { 957 | jestRunnerConfig = new JestRunnerConfig(); 958 | configFilePath = configPath && configFilename ? path.resolve(configPath, configFilename) : ''; 959 | jest 960 | .spyOn(vscode.workspace, 'getWorkspaceFolder') 961 | .mockReturnValue(new WorkspaceFolder(new Uri(workspacePath) as any) as any); 962 | activeTextEditorSpy = jest 963 | .spyOn(vscode.window, 'activeTextEditor', 'get') 964 | .mockReturnValue(new TextEditor(new Document(new Uri(openedFilePath))) as any); 965 | jest.spyOn(fs, 'existsSync').mockImplementation((filePath) => filePath === configFilePath); 966 | jest.spyOn(fs, 'statSync').mockImplementation((path): any => ({ 967 | isDirectory: () => !openedFilePath.endsWith('.ts'), 968 | })); 969 | }); 970 | 971 | its[_os](behavior, async () => { 972 | if (configPath) { 973 | expect(jestRunnerConfig.findConfigPath()).toBe(normalizePath(configFilePath)); 974 | } else { 975 | expect(jestRunnerConfig.findConfigPath()).toBeUndefined(); 976 | } 977 | expect(activeTextEditorSpy).toBeCalled(); 978 | }); 979 | }, 980 | ); 981 | }); 982 | describe('targetPath is provided', () => { 983 | describe.each(scenarios)( 984 | '%s: %s', 985 | (_os, _name, behavior, workspacePath, openedFilePath, configPath, configFilename) => { 986 | let jestRunnerConfig: JestRunnerConfig; 987 | let configFilePath: string; 988 | 989 | beforeEach(() => { 990 | jestRunnerConfig = new JestRunnerConfig(); 991 | configFilePath = configPath && configFilename ? path.resolve(configPath, configFilename) : ''; 992 | jest.spyOn(vscode.window, 'activeTextEditor', 'get'); 993 | jest 994 | .spyOn(vscode.workspace, 'getWorkspaceFolder') 995 | .mockReturnValue(new WorkspaceFolder(new Uri(workspacePath) as any) as any); 996 | jest.spyOn(fs, 'existsSync').mockImplementation((filePath) => filePath === configFilePath); 997 | jest.spyOn(fs, 'statSync').mockImplementation((path): any => ({ 998 | isDirectory: () => !openedFilePath.endsWith('.ts'), 999 | })); 1000 | }); 1001 | 1002 | its[_os](behavior, async () => { 1003 | if (configPath) { 1004 | expect(jestRunnerConfig.findConfigPath(openedFilePath)).toBe(normalizePath(configFilePath)); 1005 | } else { 1006 | expect(jestRunnerConfig.findConfigPath(openedFilePath)).toBeUndefined(); 1007 | } 1008 | }); 1009 | }, 1010 | ); 1011 | }); 1012 | }); 1013 | }); 1014 | --------------------------------------------------------------------------------