├── .eslintrc.js ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── node-ci.yml │ └── stale.yml ├── .gitignore ├── .vscode-insiders └── argv.json ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __mocks__ ├── istanbul-lib-source-maps.ts └── vscode.ts ├── icons ├── coverage-on-20.svg └── pause-on-20.svg ├── images ├── auto-complete.gif ├── autoRun-tradeoff.jpg ├── coverage-DefaultFormatter.png ├── coverage-GutterFormatter.png ├── coverage-screen-shot.png ├── interactive-watch-mode.png ├── interface-6.1.0.png ├── main-features-5.0.png ├── quick-fix-chooser.png ├── release-notes-5.x-reveal-output-button.png ├── run-debug.png ├── run-test.png ├── runmode-chooser.png ├── runmode-tradeoff.png ├── setup-tool │ ├── main-menu.png │ ├── monorepo-menu.png │ ├── setup-debug-config.png │ └── setup-jest-command.png ├── snapshot-menu.png ├── status-bar-error.png ├── status-bar-overview.png ├── test-coverage-explorer.png ├── testExplorer-6.0.2.png ├── v5-output-terminals.png ├── v6-quick-start.png ├── v6.2.0-intro.png ├── vscode-jest-env-log.gif └── vscode-jest.png ├── jest.config.js ├── jsconfig.json ├── language-configuration.json ├── package.json ├── prettier.config.js ├── release-notes ├── release-note-v4.md ├── release-note-v5.1.md ├── release-note-v5.md ├── release-note-v5.x.md ├── release-note-v6.md └── release-notes.md ├── scripts ├── compare-coverage.ts └── orphan-images.sh ├── setup-wizard.md ├── src ├── Coverage │ ├── CoverageCodeLensProvider.ts │ ├── CoverageMapProvider.ts │ ├── CoverageOverlay.ts │ ├── Formatters │ │ ├── AbstractFormatter.ts │ │ ├── DefaultFormatter.ts │ │ ├── GutterFormatter │ │ │ ├── coverage.svg │ │ │ └── index.ts │ │ └── helpers.ts │ └── index.ts ├── DebugConfigurationProvider.ts ├── JestExt │ ├── core.ts │ ├── helper.ts │ ├── index.ts │ ├── output-terminal.ts │ ├── process-listeners.ts │ ├── process-session.ts │ ├── run-mode.ts │ ├── run-shell.ts │ └── types.ts ├── JestProcessManagement │ ├── JestProcess.ts │ ├── JestProcessManager.ts │ ├── helper.ts │ ├── index.ts │ ├── task-queue.ts │ └── types.ts ├── Settings │ ├── helper.ts │ ├── index.ts │ └── types.ts ├── StatusBar.ts ├── TestResults │ ├── TestResult.ts │ ├── TestResultProvider.ts │ ├── index.ts │ ├── match-by-context.ts │ ├── match-node.ts │ ├── snapshot-provider.ts │ └── test-result-events.ts ├── appGlobals.ts ├── diagnostics.ts ├── errors.ts ├── extension-manager.ts ├── extension.ts ├── helpers.ts ├── language-provider.ts ├── logging.ts ├── noop-fs-provider.ts ├── output-manager.ts ├── quick-fix.ts ├── reporter.ts ├── setup-wizard │ ├── index.ts │ ├── start-wizard.ts │ ├── tasks │ │ ├── index.ts │ │ ├── setup-jest-cmdline.ts │ │ ├── setup-jest-debug.ts │ │ └── setup-monorepo.ts │ ├── types.ts │ └── wizard-helper.ts ├── terminal-link-provider.ts ├── test-provider │ ├── index.ts │ ├── jest-test-run.ts │ ├── test-coverage.ts │ ├── test-item-context-manager.ts │ ├── test-item-data.ts │ ├── test-provider-context.ts │ ├── test-provider.ts │ └── types.ts ├── types.ts ├── virtual-workspace-folder.ts └── workspace-manager.ts ├── syntaxes ├── ExtSettingsSchema.json ├── LICENSE └── jest-snapshot.tmLanguage ├── tests ├── .eslintrc.js ├── Coverage │ ├── CoverageCodeLensProvider.test.ts │ ├── CoverageMapProvider.test.ts │ ├── CoverageOverlay.test.ts │ └── Formatters │ │ ├── AbstractFormatter.test.ts │ │ ├── DefaultFormatter.test.ts │ │ ├── GutterFormatter.test.ts │ │ └── helpers.test.ts ├── DebugConfigurationProvider.test.ts ├── JestExt │ ├── __snapshots__ │ │ └── outpt-terminal.test.ts.snap │ ├── core.test.ts │ ├── helper.test.ts │ ├── outpt-terminal.test.ts │ ├── process-listeners.test.ts │ ├── process-session.test.ts │ ├── run-mode.test.ts │ └── run-shell.test.ts ├── JestProcessManagement │ ├── JestProcess.test.ts │ ├── JestProcessManager.test.ts │ ├── helper.test.ts │ └── test-queue.test.ts ├── Settings │ └── helper.test.ts ├── StatusBar.test.ts ├── TestResults │ ├── TestResult.test.ts │ ├── TestResultProvider.test.ts │ ├── match-by-context.test.ts │ ├── match-node.test.ts │ └── snapshot-provider.test.ts ├── __snapshots__ │ └── helpers.test.ts.snap ├── diagnostics.test.ts ├── extension-manager.test.ts ├── extension.test.ts ├── fileMock.ts ├── helpers.test.ts ├── language-provider.test.ts ├── logging.test.ts ├── manual-mocks.ts ├── mock-platform.ts ├── output-manager.test.ts ├── quick-fix.test.ts ├── reporter.test.ts ├── setup-wizard │ ├── start-wizard.test.ts │ ├── tasks │ │ ├── setup-jest-cmdline.test.ts │ │ ├── setup-jest-debug.test.ts │ │ ├── setup-monorepo.test.ts │ │ └── task-test-helper.ts │ ├── test-helper.ts │ └── wizard-helper.test.ts ├── terminal-link-provider.test.ts ├── test-helper.ts ├── test-provider │ ├── jest-test-runt.test.ts │ ├── test-coverage.test.ts │ ├── test-helper.ts │ ├── test-item-context-manager.test.ts │ ├── test-item-data.test.ts │ ├── test-provider-context.test.ts │ └── test-provider.test.ts ├── tsconfig.json ├── virtual-workspace-folder.test.ts └── workspace-manager.test.ts ├── tsconfig.json ├── tsconfig.prod.json ├── typings ├── custom.d.ts └── vscode.d.ts ├── webpack ├── dummy-module.js ├── jest-snapshot-loader.js └── webpack.config.js └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | es6: true, 5 | node: true, 6 | 'jest/globals': true, 7 | }, 8 | parser: '@typescript-eslint/parser', 9 | plugins: ['@typescript-eslint', 'jest', 'prettier'], 10 | extends: [ 11 | 'eslint:recommended', 12 | 'plugin:@typescript-eslint/eslint-recommended', 13 | 'plugin:@typescript-eslint/recommended', 14 | 'plugin:jest/recommended', 15 | 'prettier', 16 | 'plugin:prettier/recommended', 17 | ], 18 | rules: { 19 | 'prettier/prettier': 'error', 20 | // too many tests to fix, disable for now 21 | '@typescript-eslint/ban-types': 'off', 22 | // customize argument ignore pattern 23 | 'no-unused-vars': 'off', 24 | '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], 25 | }, 26 | overrides: [ 27 | { 28 | files: ['*.js'], 29 | rules: { 30 | '@typescript-eslint/no-var-requires': 'off', 31 | '@typescript-eslint/explicit-function-return-type': 'off', 32 | }, 33 | }, 34 | ], 35 | reportUnusedDisableDirectives: true, 36 | }; 37 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # all text files will be checked in with LF as eol 2 | * text=auto -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | ### Environment 3 | 4 | 1. `vscode-jest version`: [fill] 5 | 2. `node -v`: [fill] 6 | 3. `npm -v` or `yarn --version`: [fill] 7 | 4. `npm ls jest` or `npm ls react-scripts` (if you haven’t ejected): [fill] 8 | 5. your vscode-jest settings if customized: 9 | - jest.jestCommandLine? [fill] 10 | - jest.autoRun? [fill] 11 | - anything else that you think might be relevant? [fill] 12 | 6. Operating system: [fill] 13 | 14 | ### Prerequisite 15 | - are you able to run jest test from the command line? [fill] 16 | - how do you run your tests from the command line? (for example: `npm run test` or `node_modules/.bin/jest`) [fill] 17 | 18 | ### Steps to Reproduce 19 | 20 | 21 | 22 | [fill] 23 | 24 | ### Relevant Debug Info 25 | 26 | 27 | 28 | [fill] 29 | 30 | ### Expected Behavior 31 | 32 | [fill] 33 | 34 | 35 | ### Actual Behavior 36 | 37 | [fill] 38 | 39 | 40 | --- 41 | 42 | The fastest (and the most fun) way to resolve the issue is to submit a pull-request yourself. If you are interested, feel free to check out the [contribution guide](https://github.com/jest-community/vscode-jest/CONTRIBUTING.md), we look forward to seeing your PR... 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | Note: A sample repo will help us identify the bug much faster. 🙏 21 | 22 | **Expected behavior** 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Screenshots** 26 | If applicable, add screenshots to help explain your problem. 27 | 28 | **Environment (please complete the following information):** 29 | - vscode-jest version:[e.g., v6.2.1] 30 | - `node -v`: [e.g., 20.9.0] 31 | - `npm -v` or `yarn --version`: [e.g., npm 10.1.0] 32 | - jest or react-scripts (if you haven’t ejected) version: [e.g., jest 29.7.0] 33 | - your vscode-jest settings: 34 | - jest.jestCommandLine? [e.g., npm test --] 35 | - jest.runMode? [e.g., on-demand] 36 | - jest.outputConfig? [e.g., none] 37 | - anything else that you think might be relevant? [e.g., none] 38 | - Operating system: [e.g., MacOS 14.2.1] 39 | 40 | **Prerequisite** 41 | - are you able to run jest from the command line? [e.g., yes] 42 | - where do you run jest CLI from? [e.g., root directory of the project] 43 | - how do you run your tests from the command line? (e.g., "npm run test" or "node_modules/.bin/jest") 44 | 45 | **Additional context** 46 | Add any other context about the problem here. 47 | 48 | --- 49 | 50 | The fastest (and the most fun) way to resolve the issue is to submit a pull request yourself. If you are interested, please check out the [contribution guide](https://github.com/jest-community/vscode-jest/CONTRIBUTING.md), we look forward to seeing your PR... 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/node-ci.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | cleanup-runs: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: rokroskar/workflow-run-cleanup-action@master 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/master'" 17 | 18 | prepare-yarn-cache: 19 | name: Prepare yarn cache 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version: 18.x 28 | cache: yarn 29 | 30 | - name: Validate cache 31 | run: yarn --immutable 32 | 33 | lint-typecheck: 34 | name: Pre-test checks 35 | runs-on: ubuntu-latest 36 | needs: prepare-yarn-cache 37 | 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: actions/setup-node@v4 41 | with: 42 | node-version: 18.x 43 | cache: yarn 44 | - name: install 45 | run: yarn --immutable 46 | 47 | - name: run tsc 48 | run: yarn tsc 49 | - name: run eslint 50 | run: yarn lint 51 | 52 | test: 53 | name: Testing on ${{ matrix.os }} with Node v${{ matrix.node-version }} 54 | strategy: 55 | matrix: 56 | node-version: [18.x, 20.x] 57 | os: [ubuntu-latest, macOS-latest, windows-latest] 58 | runs-on: ${{ matrix.os }} 59 | needs: prepare-yarn-cache 60 | 61 | steps: 62 | - uses: actions/checkout@v2 63 | - name: Use Node.js ${{ matrix.node-version }} 64 | uses: actions/setup-node@v4 65 | with: 66 | node-version: ${{ matrix.node-version }} 67 | cache: yarn 68 | - name: install 69 | run: yarn --immutable 70 | 71 | - name: test prepublish 72 | run: yarn vscode:prepublish 73 | 74 | - name: test 75 | run: yarn test 76 | 77 | coverage: 78 | name: Checking Coverage 79 | runs-on: ubuntu-latest 80 | needs: prepare-yarn-cache 81 | 82 | steps: 83 | - uses: actions/checkout@v2 84 | - uses: actions/setup-node@v4 85 | with: 86 | node-version: 18.x 87 | cache: yarn 88 | - name: install 89 | run: yarn --immutable 90 | 91 | - name: test with coverage 92 | run: yarn test --coverage 93 | 94 | - name: Coveralls 95 | uses: coverallsapp/github-action@master 96 | with: 97 | github-token: ${{ secrets.GITHUB_TOKEN }} 98 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Manage Stale, Legacy, and Close Issues 2 | 3 | on: 4 | # Uncomment the schedule below to run automatically, or trigger manually with workflow_dispatch. 5 | # schedule: 6 | # - cron: '0 4 * * *' # 04:00 UTC 7 | 8 | # Uncomment below and enable debug-only when testing changes via PR 9 | # push: 10 | # branches: 11 | # - manage-stale-issues 12 | # pull_request: 13 | # branches: 14 | # - manage-stale-issues 15 | 16 | workflow_dispatch: 17 | 18 | jobs: 19 | # ---------------------------------------------------------------------------- 20 | # Job: stale-issues 21 | # Summary: 22 | # Marks open issues as "stale" if they have had no activity for 1 year (365 days), 23 | # and then closes issues that remain stale for 30 days, 24 | # unless they carry exempt labels such as "pinned", "security", or "keep-alive". 25 | # ---------------------------------------------------------------------------- 26 | stale-issues: 27 | runs-on: ubuntu-latest 28 | permissions: 29 | issues: write 30 | steps: 31 | - name: Manage stale issues 32 | uses: actions/stale@v9 33 | with: 34 | repo-token: ${{ secrets.GITHUB_TOKEN }} 35 | days-before-issue-stale: 365 36 | days-before-pr-stale: -1 37 | days-before-issue-close: 30 38 | days-before-pr-close: -1 39 | stale-issue-label: 'stale' 40 | close-issue-label: 'auto-closed' 41 | stale-issue-message: > 42 | This issue has been inactive for over a year and has been marked as stale. 43 | It will be automatically closed in 30 days if no further activity occurs. 44 | Since significant changes have occurred in the codebase, please open a new issue 45 | with updated details if the problem still persists. 46 | close-issue-message: > 47 | This issue is being automatically closed due to prolonged inactivity. 48 | If you believe this issue is still relevant, please open a new issue with updated details. 49 | exempt-issue-labels: 'pinned,security,keep-alive' 50 | debug-only: false 51 | enable-statistics: true 52 | remove-stale-when-updated: true 53 | operations-per-run: 1000 54 | 55 | 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _jest-editor/ 2 | .vscode/symbols.json 3 | /coverage*/ 4 | node_modules/ 5 | out/ 6 | generated-icons/ 7 | 8 | **/.DS_Store 9 | *.vsix 10 | *.zip 11 | coverage_comparison_report.html 12 | yarn-error.log 13 | 14 | -------------------------------------------------------------------------------- /.vscode-insiders/argv.json: -------------------------------------------------------------------------------- 1 | { 2 | "enable-proposed-api": ["orta.vscode-jest"] 3 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Debug Tests", 8 | "program": "${workspaceRoot}/node_modules/.bin/jest", 9 | "cwd": "${workspaceRoot}", 10 | "args": ["--i", "--config", "jest.config.js"], 11 | "windows": { 12 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 13 | } 14 | }, 15 | { 16 | "name": "Launch Extension (development)", 17 | "type": "extensionHost", 18 | "request": "launch", 19 | "runtimeExecutable": "${execPath}", 20 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 21 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 22 | "preLaunchTask": "watch" 23 | }, 24 | { 25 | "name": "Launch Extension (production)", 26 | "type": "extensionHost", 27 | "request": "launch", 28 | "runtimeExecutable": "${execPath}", 29 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 30 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 31 | "preLaunchTask": "npm: compile" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "out": true, 4 | "node_modules/jest-editor-support/src": false, 5 | "coverage/": true 6 | }, 7 | "search.exclude": { 8 | "out": true, 9 | "node_modules/jest-editor-support/src": false 10 | }, 11 | "typescript.tsdk": "./node_modules/typescript/lib", 12 | // jest 13 | 14 | // eslint 15 | "tslint.enable": false, 16 | "eslint.enable": true, 17 | "editor.codeActionsOnSave": { 18 | "source.fixAll.eslint": "explicit" 19 | }, 20 | "cSpell.words": [ 21 | "unmock" 22 | ], 23 | "markdown.validate.ignoredLinks": [ 24 | "#runmode-chooser", 25 | "#runmode-tradeoff", 26 | "#quick-fix-chooser", 27 | "#virtual-folders", 28 | "#outputconfig-conflict", 29 | "#runmode-migration", 30 | "#auto-recovery-login-shell" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "watch", 6 | "type": "shell", 7 | "command": "yarn run watch", 8 | "isBackground": true, 9 | "problemMatcher": [ 10 | { 11 | "owner": "custom", 12 | "pattern": [ 13 | { 14 | "regexp": "^PATTERN WON'T MATCH, BUT NEEDED TO BE A VALID PROBLEM MATCHER$", 15 | "file": 1, 16 | "location": 2, 17 | "message": 3 18 | } 19 | ], 20 | "background": { 21 | "activeOnStart": true, 22 | "beginsPattern": ".", 23 | "endsPattern": "^webpack.+compiled" 24 | } 25 | } 26 | ] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-insiders/** 3 | .vscode-test/** 4 | src/** 5 | **/*.map 6 | .gitignore 7 | tsconfig.json 8 | **/__mocks__/** 9 | **/tests/** 10 | **/*.ts 11 | **/tsconfig.json 12 | jsconfig.json 13 | jest.config.js 14 | .eslintrc.js 15 | **/.eslintrc.js 16 | prettier.config.js 17 | .travis.yml 18 | yarn.lock 19 | yarn-error.log 20 | scripts/ 21 | coverage 22 | .github/** 23 | images/** 24 | !images/vscode-jest.png 25 | node_modules 26 | webpack.config.js 27 | generated-icons/ 28 | release-notes/ 29 | setup-wizard.md 30 | *.zip 31 | coverage*/ 32 | scripts/** 33 | coverage_comparison_report.html 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Orta 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 | -------------------------------------------------------------------------------- /__mocks__/istanbul-lib-source-maps.ts: -------------------------------------------------------------------------------- 1 | export = { createSourceMapStore: jest.fn() } 2 | -------------------------------------------------------------------------------- /__mocks__/vscode.ts: -------------------------------------------------------------------------------- 1 | const languages = { 2 | createDiagnosticCollection: jest.fn(), 3 | registerCodeLensProvider: jest.fn(), 4 | }; 5 | 6 | const StatusBarAlignment = { Left: 1, Right: 2 }; 7 | 8 | const window = { 9 | createStatusBarItem: jest.fn(() => ({ 10 | show: jest.fn(), 11 | hide: jest.fn(), 12 | tooltip: jest.fn(), 13 | })), 14 | showErrorMessage: jest.fn(), 15 | showWarningMessage: jest.fn(), 16 | createTextEditorDecorationType: jest.fn(), 17 | createOutputChannel: jest.fn(), 18 | showWorkspaceFolderPick: jest.fn(), 19 | showQuickPick: jest.fn(), 20 | onDidChangeActiveTextEditor: jest.fn(), 21 | showInformationMessage: jest.fn(), 22 | createWebviewPanel: jest.fn(), 23 | }; 24 | 25 | const extensions = { 26 | getExtension: jest.fn(), 27 | }; 28 | 29 | const workspace = { 30 | getConfiguration: jest.fn(), 31 | workspaceFolders: [], 32 | getWorkspaceFolder: jest.fn(), 33 | 34 | onDidChangeConfiguration: jest.fn(), 35 | onDidChangeTextDocument: jest.fn(), 36 | onDidChangeWorkspaceFolders: jest.fn(), 37 | onDidCreateFiles: jest.fn(), 38 | onDidDeleteFiles: jest.fn(), 39 | onDidRenameFiles: jest.fn(), 40 | onDidSaveTextDocument: jest.fn(), 41 | onWillSaveTextDocument: jest.fn(), 42 | }; 43 | 44 | const OverviewRulerLane = { 45 | Left: null, 46 | }; 47 | 48 | const Uri = { 49 | file: (f) => f, 50 | parse: jest.fn(), 51 | joinPath: jest.fn(), 52 | }; 53 | const Range = jest.fn(); 54 | const Location = jest.fn(); 55 | const Position = jest.fn(); 56 | const Diagnostic = jest.fn(); 57 | const ThemeIcon = jest.fn(); 58 | const DiagnosticSeverity = { Error: 0, Warning: 1, Information: 2, Hint: 3 }; 59 | const ConfigurationTarget = { Global: 1, Workspace: 2, WorkspaceFolder: 3 }; 60 | 61 | const debug = { 62 | onDidTerminateDebugSession: jest.fn(), 63 | startDebugging: jest.fn(), 64 | registerDebugConfigurationProvider: jest.fn(), 65 | }; 66 | 67 | const commands = { 68 | executeCommand: jest.fn(), 69 | registerCommand: jest.fn(), 70 | registerTextEditorCommand: jest.fn(), 71 | }; 72 | 73 | const CodeLens = function CodeLens() {}; 74 | 75 | const QuickInputButtons = { 76 | Back: {}, 77 | }; 78 | 79 | const tests = { 80 | createTestController: jest.fn(), 81 | }; 82 | 83 | const TestRunProfileKind = { 84 | Run: 1, 85 | Debug: 2, 86 | Coverage: 3, 87 | }; 88 | const ViewColumn = { 89 | One: 1, 90 | Tow: 2, 91 | }; 92 | 93 | const TestMessage = jest.fn(); 94 | const TestRunRequest = jest.fn(); 95 | const ThemeColor = jest.fn(); 96 | 97 | const EventEmitter = jest.fn().mockImplementation(() => { 98 | return { 99 | fire: jest.fn(), 100 | }; 101 | }); 102 | 103 | const QuickPickItemKind = { 104 | Separator: -1, 105 | Default: 0, 106 | }; 107 | 108 | // for coverage 109 | const FileCoverage = jest.fn(); 110 | const StatementCoverage = jest.fn(); 111 | const BranchCoverage = jest.fn(); 112 | const DeclarationCoverage = jest.fn(); 113 | const TestCoverageCount = jest.fn(); 114 | 115 | export = { 116 | extensions, 117 | ThemeColor, 118 | CodeLens, 119 | languages, 120 | StatusBarAlignment, 121 | window, 122 | workspace, 123 | OverviewRulerLane, 124 | Uri, 125 | Range, 126 | Location, 127 | Position, 128 | Diagnostic, 129 | ThemeIcon, 130 | DiagnosticSeverity, 131 | ConfigurationTarget, 132 | debug, 133 | commands, 134 | QuickInputButtons, 135 | tests, 136 | TestRunProfileKind, 137 | EventEmitter, 138 | TestMessage, 139 | TestRunRequest, 140 | ViewColumn, 141 | QuickPickItemKind, 142 | FileCoverage, 143 | StatementCoverage, 144 | BranchCoverage, 145 | DeclarationCoverage, 146 | TestCoverageCount, 147 | }; 148 | -------------------------------------------------------------------------------- /icons/coverage-on-20.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/pause-on-20.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/auto-complete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/auto-complete.gif -------------------------------------------------------------------------------- /images/autoRun-tradeoff.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/autoRun-tradeoff.jpg -------------------------------------------------------------------------------- /images/coverage-DefaultFormatter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/coverage-DefaultFormatter.png -------------------------------------------------------------------------------- /images/coverage-GutterFormatter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/coverage-GutterFormatter.png -------------------------------------------------------------------------------- /images/coverage-screen-shot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/coverage-screen-shot.png -------------------------------------------------------------------------------- /images/interactive-watch-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/interactive-watch-mode.png -------------------------------------------------------------------------------- /images/interface-6.1.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/interface-6.1.0.png -------------------------------------------------------------------------------- /images/main-features-5.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/main-features-5.0.png -------------------------------------------------------------------------------- /images/quick-fix-chooser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/quick-fix-chooser.png -------------------------------------------------------------------------------- /images/release-notes-5.x-reveal-output-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/release-notes-5.x-reveal-output-button.png -------------------------------------------------------------------------------- /images/run-debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/run-debug.png -------------------------------------------------------------------------------- /images/run-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/run-test.png -------------------------------------------------------------------------------- /images/runmode-chooser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/runmode-chooser.png -------------------------------------------------------------------------------- /images/runmode-tradeoff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/runmode-tradeoff.png -------------------------------------------------------------------------------- /images/setup-tool/main-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/setup-tool/main-menu.png -------------------------------------------------------------------------------- /images/setup-tool/monorepo-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/setup-tool/monorepo-menu.png -------------------------------------------------------------------------------- /images/setup-tool/setup-debug-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/setup-tool/setup-debug-config.png -------------------------------------------------------------------------------- /images/setup-tool/setup-jest-command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/setup-tool/setup-jest-command.png -------------------------------------------------------------------------------- /images/snapshot-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/snapshot-menu.png -------------------------------------------------------------------------------- /images/status-bar-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/status-bar-error.png -------------------------------------------------------------------------------- /images/status-bar-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/status-bar-overview.png -------------------------------------------------------------------------------- /images/test-coverage-explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/test-coverage-explorer.png -------------------------------------------------------------------------------- /images/testExplorer-6.0.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/testExplorer-6.0.2.png -------------------------------------------------------------------------------- /images/v5-output-terminals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/v5-output-terminals.png -------------------------------------------------------------------------------- /images/v6-quick-start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/v6-quick-start.png -------------------------------------------------------------------------------- /images/v6.2.0-intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/v6.2.0-intro.png -------------------------------------------------------------------------------- /images/vscode-jest-env-log.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/vscode-jest-env-log.gif -------------------------------------------------------------------------------- /images/vscode-jest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/vscode-jest/c93ec7a278ec84c2ac909d16ed155caccaeca933/images/vscode-jest.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | transform: { 4 | '^.+\\.tsx?$': [ 5 | 'ts-jest', 6 | { 7 | tsconfig: 'tests/tsconfig.json', 8 | }, 9 | ], 10 | }, 11 | testEnvironment: 'node', 12 | testRegex: 'tests/.*\\.test\\.ts$', 13 | coveragePathIgnorePatterns: ['/node_modules/', '/tests/'], 14 | automock: true, 15 | moduleFileExtensions: ['ts', 'js', 'json'], 16 | unmockedModulePathPatterns: [ 17 | 'jest-editor-support/node_modules', 18 | 'color-convert', 19 | 'chalk', 20 | 'snapdragon', 21 | 'ansi-styles', 22 | 'core-js', 23 | 'debug', 24 | '@babel/template', 25 | 'graceful-fs', 26 | '@babel/types', 27 | ], 28 | moduleNameMapper: { 29 | '\\.(svg)$': '/tests/fileMock.ts', 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=759670 3 | // for the documentation about the jsconfig.json format 4 | "compilerOptions": { 5 | "target": "es6", 6 | "module": "commonjs", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "exclude": ["node_modules", "bower_components", "jspm_packages", "tmp", "temp"] 10 | } 11 | -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "//", 4 | "blockComment": ["/*", "*/"] 5 | }, 6 | "brackets": [ 7 | ["{", "}"], 8 | ["[", "]"], 9 | ["(", ")"], 10 | ["<", ">"] 11 | ], 12 | "autoClosingPairs": [ 13 | { "open": "{", "close": "}", "notIn": ["comment"] }, 14 | { "open": "[", "close": "]", "notIn": ["comment"] }, 15 | { "open": "(", "close": ")", "notIn": ["comment"] }, 16 | { "open": "'", "close": "'", "notIn": ["comment"] }, 17 | { "open": "\"", "close": "\"", "notIn": ["comment"] }, 18 | { "open": "`", "close": "`", "notIn": ["comment"] }, 19 | { "open": "/**", "close": " */", "notIn": ["string"] } 20 | ], 21 | "surroundingPairs": [ 22 | ["{", "}"], 23 | ["[", "]"], 24 | ["(", ")"], 25 | ["'", "'"], 26 | ["\"", "\""], 27 | ["`", "`"], 28 | ["<", ">"] 29 | ], 30 | "colorizedBracketPairs": [ 31 | ["(", ")"], 32 | ["[", "]"], 33 | ["{", "}"] 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'es5', 3 | printWidth: 100, 4 | semi: true, 5 | singleQuote: true, 6 | overrides: [ 7 | { 8 | files: '*.ts', 9 | options: { 10 | parser: 'typescript', 11 | }, 12 | }, 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /release-notes/release-notes.md: -------------------------------------------------------------------------------- 1 | # vscode-jest Release Notes 2 | - [v6](release-note-v6.md) 3 | - [v5](release-note-v5.x.md) 4 | - [v4](release-note-v4.md) -------------------------------------------------------------------------------- /scripts/orphan-images.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function print_usage() { 4 | echo "Usage: $0 [OPTIONS]" 5 | echo "Find and optionally delete unused image files." 6 | echo 7 | echo "Options:" 8 | echo " -d Delete the unused image files." 9 | echo " -h Display this help message." 10 | exit 1 11 | } 12 | 13 | # If the -h argument is provided, display usage 14 | if [[ $1 == "-h" ]]; then 15 | print_usage 16 | fi 17 | 18 | # Directory where the images reside 19 | IMAGES_DIR="images" 20 | 21 | # Directories to exclude from the search 22 | EXCLUDE_DIRS=("images" ".git" ".node_modules" "out" ".vscode") # Add any other directories you wish to exclude 23 | 24 | # Construct the grep exclude pattern for directories 25 | EXCLUDE_DIR_PATTERN="" 26 | for dir in "${EXCLUDE_DIRS[@]}"; do 27 | EXCLUDE_DIR_PATTERN="$EXCLUDE_DIR_PATTERN --exclude-dir=$dir" 28 | done 29 | echo EXCLUDE_DIR_PATTERN: $EXCLUDE_DIR_PATTERN 30 | 31 | # File types to include in the search 32 | INCLUDE_FILES=("*.ts" "*.json" "*.md") # Specify the file types you want to search within 33 | 34 | # Construct the grep include pattern for files 35 | INCLUDE_FILE_PATTERN="" 36 | for file in "${INCLUDE_FILES[@]}"; do 37 | INCLUDE_FILE_PATTERN="$INCLUDE_FILE_PATTERN --include=$file" 38 | done 39 | echo INCLUDE_FILE_PATTERN: $INCLUDE_FILE_PATTERN 40 | 41 | # Temporary file for storing results 42 | TEMP_FILE="unused_images.txt" 43 | 44 | # Empty the temp file in case it already exists 45 | > $TEMP_FILE 46 | 47 | # Counter for unused images 48 | UNUSED_COUNT=0 49 | 50 | RED='\033[31m' 51 | NC='\033[0m' # No Color 52 | 53 | # Iterate over all image files 54 | while read -r img; do 55 | # Search for the image file in the specified file types and excluding the specified directories 56 | grep -rl "$img" $EXCLUDE_DIR_PATTERN $INCLUDE_FILE_PATTERN . > /dev/null 57 | 58 | # If grep's exit status is non-zero, then the image is not referenced anywhere 59 | if [ $? -ne 0 ]; then 60 | echo "$img" >> $TEMP_FILE 61 | # Increment the counter 62 | ((UNUSED_COUNT++)) 63 | echo -e "${RED}$img => not used ($UNUSED_COUNT) ${NC}" 64 | else 65 | echo -e "$img => used" 66 | fi 67 | done < <(find $IMAGES_DIR -type f \( -iname \*.jpg -o -iname \*.jpeg -o -iname \*.png -o -iname \*.gif \)) 68 | 69 | echo "-------------------------" 70 | 71 | # Display the results 72 | if [ -s $TEMP_FILE ]; then 73 | echo "$UNUSED_COUNT Unused images:" 74 | cat $TEMP_FILE 75 | echo "-------------------------" 76 | 77 | # If the -d argument is provided, delete the files 78 | if [[ $1 == "-d" ]]; then 79 | echo "Deleting unused images..." 80 | while IFS= read -r img; do 81 | rm "$img" 82 | done < $TEMP_FILE 83 | echo "Unused images deleted." 84 | fi 85 | else 86 | echo "No unused images found." 87 | fi 88 | 89 | # Clean up the temp file 90 | rm $TEMP_FILE -------------------------------------------------------------------------------- /src/Coverage/CoverageCodeLensProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { GetJestExtByURI } from '../extension-manager'; 4 | import { FileCoverage } from 'istanbul-lib-coverage'; 5 | 6 | export class CoverageCodeLensProvider implements vscode.CodeLensProvider { 7 | private getJestExt: GetJestExtByURI; 8 | private onDidChange: vscode.EventEmitter; 9 | onDidChangeCodeLenses: vscode.Event; 10 | 11 | constructor(getJestExt: GetJestExtByURI) { 12 | this.getJestExt = getJestExt; 13 | this.onDidChange = new vscode.EventEmitter(); 14 | this.onDidChangeCodeLenses = this.onDidChange.event; 15 | } 16 | 17 | private createCodeLensForCoverage(coverage: FileCoverage, name?: string): vscode.CodeLens { 18 | const summary = coverage.toSummary(); 19 | const json = summary.toJSON(); 20 | const metrics = (Object.keys(json) as Array).reduce((previous, metric) => { 21 | return `${previous}${previous ? ', ' : ''}${metric}: ${json[metric].pct}%`; 22 | }, ''); 23 | 24 | const range = new vscode.Range(0, 0, 0, 0); 25 | const command: vscode.Command = { 26 | title: name ? `${name}: ${metrics}` : metrics, 27 | command: '', 28 | }; 29 | 30 | return new vscode.CodeLens(range, command); 31 | } 32 | 33 | public provideCodeLenses( 34 | document: vscode.TextDocument 35 | ): vscode.ProviderResult { 36 | const coverages: [FileCoverage, string][] = this.getJestExt(document.uri) 37 | .map((ext) => { 38 | if (ext.coverageOverlay.enabled) { 39 | return [ext.coverageMapProvider.getFileCoverage(document.fileName), ext.name]; 40 | } 41 | }) 42 | .filter((coverageInfo) => coverageInfo?.[0] != null) as [FileCoverage, string][]; 43 | 44 | if (coverages.length === 0) { 45 | return undefined; 46 | } 47 | if (coverages.length === 1) { 48 | return [this.createCodeLensForCoverage(coverages[0][0])]; 49 | } 50 | return coverages.map(([coverage, name]) => this.createCodeLensForCoverage(coverage, name)); 51 | } 52 | public coverageChanged(): void { 53 | this.onDidChange.fire(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Coverage/CoverageMapProvider.ts: -------------------------------------------------------------------------------- 1 | import { createSourceMapStore, MapStore } from 'istanbul-lib-source-maps'; 2 | import { 3 | createCoverageMap, 4 | CoverageMap, 5 | CoverageMapData, 6 | FileCoverage, 7 | } from 'istanbul-lib-coverage'; 8 | 9 | export class CoverageMapProvider { 10 | private mapStore!: MapStore; 11 | 12 | /** 13 | * Transformed coverage map 14 | */ 15 | private _map!: CoverageMap; 16 | 17 | constructor() { 18 | this.reset(); 19 | } 20 | 21 | reset(): void { 22 | this._map = createCoverageMap(); 23 | this.mapStore = createSourceMapStore(); 24 | } 25 | get map(): CoverageMap { 26 | return this._map; 27 | } 28 | 29 | async update(obj?: CoverageMap | CoverageMapData): Promise { 30 | const map = createCoverageMap(obj); 31 | const transformed = await this.mapStore.transformCoverage(map); 32 | if (this._map) { 33 | transformed.files().forEach((fileName) => { 34 | this.setFileCoverage(fileName, transformed); 35 | }); 36 | } else { 37 | this._map = transformed; 38 | } 39 | } 40 | 41 | setFileCoverage(filePath: string, map: CoverageMap): void { 42 | this._map.data[filePath] = map.fileCoverageFor(filePath); 43 | } 44 | public getFileCoverage(filePath: string): FileCoverage { 45 | return this._map.data[filePath] as FileCoverage; 46 | } 47 | public onVisibilityChanged(visible: boolean): void { 48 | if (!visible) { 49 | this.reset(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Coverage/CoverageOverlay.ts: -------------------------------------------------------------------------------- 1 | import { AbstractFormatter } from './Formatters/AbstractFormatter'; 2 | import { CoverageMapProvider } from './CoverageMapProvider'; 3 | import { DefaultFormatter } from './Formatters/DefaultFormatter'; 4 | import { GutterFormatter } from './Formatters/GutterFormatter'; 5 | import * as vscode from 'vscode'; 6 | 7 | export type CoverageStatus = 'covered' | 'partially-covered' | 'uncovered'; 8 | export type CoverageColors = { 9 | [key in CoverageStatus]?: string; 10 | }; 11 | 12 | export class CoverageOverlay { 13 | static readonly defaultVisibility = false; 14 | static readonly defaultFormatter = 'DefaultFormatter'; 15 | formatter: AbstractFormatter; 16 | private _enabled: boolean; 17 | 18 | constructor( 19 | context: vscode.ExtensionContext, 20 | coverageMapProvider: CoverageMapProvider, 21 | enabled: boolean = CoverageOverlay.defaultVisibility, 22 | coverageFormatter: string = CoverageOverlay.defaultFormatter, 23 | colors?: CoverageColors 24 | ) { 25 | this._enabled = enabled; 26 | switch (coverageFormatter) { 27 | case 'GutterFormatter': 28 | this.formatter = new GutterFormatter(context, coverageMapProvider, colors); 29 | break; 30 | 31 | default: 32 | this.formatter = new DefaultFormatter(coverageMapProvider, colors); 33 | break; 34 | } 35 | } 36 | 37 | get enabled(): boolean { 38 | return this._enabled; 39 | } 40 | 41 | /** give formatter opportunity to dispose the decorators */ 42 | dispose(): void { 43 | this.formatter.dispose(); 44 | } 45 | 46 | updateVisibleEditors(): void { 47 | for (const editor of vscode.window.visibleTextEditors) { 48 | this.update(editor); 49 | } 50 | } 51 | 52 | update(editor: vscode.TextEditor): void { 53 | if (!editor.document) { 54 | return; 55 | } 56 | 57 | if (this._enabled) { 58 | this.formatter.format(editor); 59 | } else { 60 | this.formatter.clear(editor); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Coverage/Formatters/AbstractFormatter.ts: -------------------------------------------------------------------------------- 1 | import { CoverageMapProvider } from '../CoverageMapProvider'; 2 | import * as vscode from 'vscode'; 3 | import { CoverageColors, CoverageStatus } from '../CoverageOverlay'; 4 | import { FileCoverage } from 'istanbul-lib-coverage'; 5 | 6 | export type CoverageRanges = Partial>; 7 | 8 | type FunctionCoverageByLine = { [line: number]: number }; 9 | export abstract class AbstractFormatter { 10 | protected coverageMapProvider: CoverageMapProvider; 11 | protected colors?: CoverageColors; 12 | 13 | constructor(coverageMapProvider: CoverageMapProvider, colors?: CoverageColors) { 14 | this.coverageMapProvider = coverageMapProvider; 15 | this.colors = colors; 16 | } 17 | 18 | abstract format(editor: vscode.TextEditor): void; 19 | /** remove decorators for the given editor */ 20 | abstract clear(editor: vscode.TextEditor): void; 21 | /** dispose decorators for all editors */ 22 | abstract dispose(): void; 23 | 24 | /** 25 | * returns rgba color string similar to istanbul html report color scheme 26 | * @param status 27 | * @param opacity 28 | */ 29 | getColorString(status: CoverageStatus, opacity: number): string { 30 | if (opacity > 1 || opacity < 0) { 31 | throw new Error(`invalid opacity (${opacity}): value is not between 0 - 1`); 32 | } 33 | 34 | switch (status) { 35 | case 'covered': 36 | return this.colors?.[status] ?? `rgba(9, 156, 65, ${opacity})`; // green 37 | case 'partially-covered': 38 | return this.colors?.[status] ?? `rgba(235, 198, 52, ${opacity})`; // yellow 39 | case 'uncovered': 40 | return this.colors?.[status] ?? `rgba(121, 31, 10, ${opacity})`; // red 41 | } 42 | } 43 | 44 | private getFunctionCoverageByLine(fileCoverage: FileCoverage): FunctionCoverageByLine { 45 | const lineCoverage: FunctionCoverageByLine = {}; 46 | Object.entries(fileCoverage.fnMap).forEach(([k, { decl }]) => { 47 | const hits = fileCoverage.f[k]; 48 | for (let idx = decl.start.line; idx <= decl.end.line; idx++) { 49 | lineCoverage[idx] = hits; 50 | } 51 | }); 52 | return lineCoverage; 53 | } 54 | /** 55 | * mapping the coverage map to a line-based coverage ranges 56 | * the coverage format is based on instanbuljs: https://github.com/istanbuljs/istanbuljs/blob/master/docs/raw-output.md 57 | * @param editor 58 | */ 59 | lineCoverageRanges( 60 | editor: vscode.TextEditor, 61 | onNoCoverageInfo?: () => CoverageStatus 62 | ): CoverageRanges { 63 | const ranges: CoverageRanges = {}; 64 | const fileCoverage = this.coverageMapProvider.getFileCoverage(editor.document.fileName); 65 | if (!fileCoverage) { 66 | return ranges; 67 | } 68 | const lineCoverage = fileCoverage.getLineCoverage(); 69 | const branchCoverage = fileCoverage.getBranchCoverageByLine(); 70 | const funcCoverage = this.getFunctionCoverageByLine(fileCoverage); 71 | 72 | // consolidate the coverage by line 73 | for (let line = 1; line <= editor.document.lineCount; line++) { 74 | const zeroBasedLineNumber = line - 1; 75 | const lc = lineCoverage[line]; 76 | const bc = branchCoverage[line]; 77 | const fc = funcCoverage[line]; 78 | const statusList: CoverageStatus[] = []; 79 | if (fc != null) { 80 | statusList.push(fc > 0 ? 'covered' : 'uncovered'); 81 | } 82 | if (bc != null) { 83 | switch (bc.coverage) { 84 | case 100: 85 | statusList.push('covered'); 86 | break; 87 | case 0: 88 | statusList.push('uncovered'); 89 | break; 90 | default: 91 | statusList.push('partially-covered'); 92 | break; 93 | } 94 | } 95 | if (lc != null) { 96 | statusList.push(lc > 0 ? 'covered' : 'uncovered'); 97 | } 98 | if (statusList.length <= 0 && onNoCoverageInfo) { 99 | statusList.push(onNoCoverageInfo()); 100 | } 101 | 102 | if (statusList.length <= 0) { 103 | continue; 104 | } 105 | // sort by severity: uncovered > partially-covered > covered 106 | statusList.sort((s1, s2) => { 107 | if (s1 === s2) { 108 | return 0; 109 | } 110 | switch (s1) { 111 | case 'covered': 112 | return 1; 113 | case 'partially-covered': 114 | return s2 === 'covered' ? -1 : 1; 115 | case 'uncovered': 116 | return -1; 117 | } 118 | }); 119 | const status = statusList[0]; 120 | 121 | const range = new vscode.Range(zeroBasedLineNumber, 0, zeroBasedLineNumber, 0); 122 | const list = ranges[status]; 123 | if (list) { 124 | list.push(range); 125 | } else { 126 | ranges[status] = [range]; 127 | } 128 | } 129 | return ranges; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Coverage/Formatters/DefaultFormatter.ts: -------------------------------------------------------------------------------- 1 | import { AbstractFormatter } from './AbstractFormatter'; 2 | import * as vscode from 'vscode'; 3 | import { CoverageMapProvider } from '../CoverageMapProvider'; 4 | import { CoverageColors } from '../CoverageOverlay'; 5 | 6 | export class DefaultFormatter extends AbstractFormatter { 7 | readonly uncoveredLine: vscode.TextEditorDecorationType; 8 | readonly partiallyCoveredLine: vscode.TextEditorDecorationType; 9 | 10 | constructor(coverageMapProvider: CoverageMapProvider, colors?: CoverageColors) { 11 | super(coverageMapProvider, colors); 12 | this.partiallyCoveredLine = vscode.window.createTextEditorDecorationType({ 13 | isWholeLine: true, 14 | backgroundColor: this.getColorString('partially-covered', 0.4), 15 | overviewRulerColor: this.getColorString('partially-covered', 0.8), 16 | overviewRulerLane: vscode.OverviewRulerLane.Left, 17 | }); 18 | this.uncoveredLine = vscode.window.createTextEditorDecorationType({ 19 | isWholeLine: true, 20 | backgroundColor: this.getColorString('uncovered', 0.4), 21 | overviewRulerColor: this.getColorString('uncovered', 0.8), 22 | overviewRulerLane: vscode.OverviewRulerLane.Left, 23 | }); 24 | } 25 | 26 | format(editor: vscode.TextEditor): void { 27 | const coverageRanges = this.lineCoverageRanges(editor); 28 | editor.setDecorations(this.uncoveredLine, coverageRanges['uncovered'] ?? []); 29 | editor.setDecorations(this.partiallyCoveredLine, coverageRanges['partially-covered'] ?? []); 30 | } 31 | 32 | clear(editor: vscode.TextEditor): void { 33 | editor.setDecorations(this.uncoveredLine, []); 34 | editor.setDecorations(this.partiallyCoveredLine, []); 35 | } 36 | dispose(): void { 37 | this.partiallyCoveredLine.dispose(); 38 | this.uncoveredLine.dispose(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Coverage/Formatters/GutterFormatter/coverage.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/Coverage/Formatters/GutterFormatter/index.ts: -------------------------------------------------------------------------------- 1 | import { CoverageMapProvider } from '../../CoverageMapProvider'; 2 | import { AbstractFormatter } from '../AbstractFormatter'; 3 | import * as vscode from 'vscode'; 4 | import { prepareIconFile } from '../../../helpers'; 5 | import coverageGutterIcon from './coverage.svg'; 6 | import { CoverageColors } from '../../CoverageOverlay'; 7 | 8 | export class GutterFormatter extends AbstractFormatter { 9 | readonly uncoveredLine: vscode.TextEditorDecorationType; 10 | readonly partiallyCoveredLine: vscode.TextEditorDecorationType; 11 | readonly coveredLine: vscode.TextEditorDecorationType; 12 | 13 | constructor( 14 | context: vscode.ExtensionContext, 15 | coverageMapProvider: CoverageMapProvider, 16 | colors?: CoverageColors 17 | ) { 18 | super(coverageMapProvider, colors); 19 | 20 | const coveredColor = this.getColorString('covered', 0.75); 21 | const uncoveredColor = this.getColorString('uncovered', 0.75); 22 | const partiallyCoveredColor = this.getColorString('partially-covered', 0.75); 23 | this.uncoveredLine = vscode.window.createTextEditorDecorationType({ 24 | overviewRulerColor: uncoveredColor, 25 | overviewRulerLane: vscode.OverviewRulerLane.Left, 26 | gutterIconPath: this.iconUri(context, 'uncovered', coverageGutterIcon, uncoveredColor), 27 | }); 28 | 29 | this.partiallyCoveredLine = vscode.window.createTextEditorDecorationType({ 30 | overviewRulerColor: partiallyCoveredColor, 31 | overviewRulerLane: vscode.OverviewRulerLane.Left, 32 | gutterIconPath: this.iconUri( 33 | context, 34 | 'partially-covered', 35 | coverageGutterIcon, 36 | partiallyCoveredColor 37 | ), 38 | }); 39 | 40 | this.coveredLine = vscode.window.createTextEditorDecorationType({ 41 | gutterIconPath: this.iconUri(context, 'covered', coverageGutterIcon, coveredColor), 42 | }); 43 | } 44 | // convert iconPath to uri to prevent render cache icon by fileName alone 45 | // even after file content has changed such as color changed 46 | private iconUri( 47 | context: vscode.ExtensionContext, 48 | iconName: string, 49 | source: string, 50 | color: string 51 | ): vscode.Uri { 52 | const iconPath = prepareIconFile(context, iconName, source, color); 53 | return vscode.Uri.file(iconPath).with({ query: `color=${color}` }); 54 | } 55 | format(editor: vscode.TextEditor): void { 56 | const coverageRanges = this.lineCoverageRanges(editor, () => 'covered'); 57 | editor.setDecorations(this.uncoveredLine, coverageRanges['uncovered'] ?? []); 58 | editor.setDecorations(this.partiallyCoveredLine, coverageRanges['partially-covered'] ?? []); 59 | editor.setDecorations(this.coveredLine, coverageRanges['covered'] ?? []); 60 | } 61 | 62 | clear(editor: vscode.TextEditor): void { 63 | editor.setDecorations(this.coveredLine, []); 64 | editor.setDecorations(this.partiallyCoveredLine, []); 65 | editor.setDecorations(this.uncoveredLine, []); 66 | } 67 | 68 | dispose(): void { 69 | this.coveredLine.dispose(); 70 | this.partiallyCoveredLine.dispose(); 71 | this.uncoveredLine.dispose(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Coverage/Formatters/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Location, Range } from 'istanbul-lib-coverage'; 2 | 3 | export function isValidPosition(p: Location): boolean { 4 | return (p || false) && p.line !== null && p.line >= 0; 5 | } 6 | 7 | export function isValidLocation(l: Range): boolean { 8 | return isValidPosition(l.start) && isValidPosition(l.end); 9 | } 10 | -------------------------------------------------------------------------------- /src/Coverage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CoverageMapProvider'; 2 | export * from './CoverageOverlay'; 3 | export * from './CoverageCodeLensProvider'; 4 | -------------------------------------------------------------------------------- /src/JestExt/helper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * collection of stateless utility functions for de-clutter and easy to test 3 | */ 4 | import * as vscode from 'vscode'; 5 | import { ProjectWorkspace, LoginShell, JESParserPluginOptions } from 'jest-editor-support'; 6 | import { JestProcessRequest } from '../JestProcessManagement'; 7 | import { 8 | PluginResourceSettings, 9 | TestExplorerConfig, 10 | NodeEnv, 11 | MonitorLongRun, 12 | JestExtAutoRunSetting, 13 | createJestSettingGetter, 14 | JestRunModeType, 15 | JestRunMode, 16 | DeprecatedPluginResourceSettings, 17 | } from '../Settings'; 18 | import { workspaceLogging } from '../logging'; 19 | import { JestExtContext, RunnerWorkspaceOptions } from './types'; 20 | import { CoverageColors } from '../Coverage'; 21 | import { userInfo } from 'os'; 22 | import { JestOutputTerminal } from './output-terminal'; 23 | import { RunMode } from './run-mode'; 24 | import { RunShell } from './run-shell'; 25 | import { toAbsoluteRootPath, toFilePath } from '../helpers'; 26 | 27 | export const isWatchRequest = (request: JestProcessRequest): boolean => 28 | request.type === 'watch-tests' || request.type === 'watch-all-tests'; 29 | 30 | const getUserIdString = (): string => { 31 | try { 32 | const user = userInfo(); 33 | if (user.uid >= 0) { 34 | return user.uid.toString(); 35 | } 36 | if (user.username.length > 0) { 37 | return user.username; 38 | } 39 | } catch (e) { 40 | console.warn('failed to get userInfo:', e); 41 | } 42 | return 'unknown'; 43 | }; 44 | export const outputFileSuffix = (ws: string, extra?: string): string => { 45 | const s = `${ws}_${getUserIdString()}${extra ? `_${extra}` : ''}`; 46 | // replace non-word with '_' 47 | return s.replace(/\W/g, '_'); 48 | }; 49 | export const collectCoverage = (coverage?: boolean, settings?: PluginResourceSettings) => 50 | coverage ?? settings?.runMode.config.coverage ?? false; 51 | 52 | export const createJestExtContext = ( 53 | workspaceFolder: vscode.WorkspaceFolder, 54 | settings: PluginResourceSettings, 55 | output: JestOutputTerminal 56 | ): JestExtContext => { 57 | const createRunnerWorkspace = (options?: RunnerWorkspaceOptions) => { 58 | const ws = workspaceFolder.name; 59 | const currentJestVersion = 20; 60 | 61 | if (!settings.jestCommandLine) { 62 | throw new Error(`[${workspaceFolder.name}] missing jestCommandLine`); 63 | } 64 | return new ProjectWorkspace( 65 | toFilePath(settings.rootPath), 66 | settings.jestCommandLine, 67 | '', 68 | currentJestVersion, 69 | outputFileSuffix(ws, options?.outputFileSuffix), 70 | collectCoverage(options?.collectCoverage, settings), 71 | settings.debugMode, 72 | settings.nodeEnv, 73 | settings.shell.toSetting(), 74 | settings.useDashedArgs 75 | ); 76 | }; 77 | return { 78 | workspace: workspaceFolder, 79 | settings, 80 | createRunnerWorkspace, 81 | loggingFactory: workspaceLogging(workspaceFolder.name, settings.debugMode ?? false), 82 | output, 83 | }; 84 | }; 85 | 86 | export const getExtensionResourceSettings = ( 87 | workspaceFolder: vscode.WorkspaceFolder 88 | ): PluginResourceSettings => { 89 | const getSetting = createJestSettingGetter(workspaceFolder); 90 | 91 | const deprecatedSettings: DeprecatedPluginResourceSettings = { 92 | showCoverageOnLoad: getSetting('showCoverageOnLoad') ?? false, 93 | autoRun: getSetting('autoRun'), 94 | testExplorer: getSetting('testExplorer'), 95 | }; 96 | 97 | return { 98 | jestCommandLine: getSetting('jestCommandLine'), 99 | rootPath: toAbsoluteRootPath(workspaceFolder, getSetting('rootPath')), 100 | coverageFormatter: getSetting('coverageFormatter') ?? 'DefaultFormatter', 101 | debugMode: getSetting('debugMode'), 102 | coverageColors: getSetting('coverageColors'), 103 | nodeEnv: getSetting('nodeEnv') ?? undefined, 104 | shell: new RunShell(getSetting('shell')), 105 | monitorLongRun: getSetting('monitorLongRun') ?? undefined, 106 | runMode: new RunMode( 107 | getSetting('runMode'), 108 | deprecatedSettings 109 | ), 110 | parserPluginOptions: getSetting('parserPluginOptions'), 111 | enable: getSetting('enable'), 112 | useDashedArgs: getSetting('useDashedArgs') ?? false, 113 | useJest30: getSetting('useJest30'), 114 | }; 115 | }; 116 | 117 | export const prefixWorkspace = (context: JestExtContext, message: string): string => { 118 | if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 1) { 119 | return `(${context.workspace.name}) ${message}`; 120 | } 121 | return message; 122 | }; 123 | -------------------------------------------------------------------------------- /src/JestExt/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /src/JestExt/run-shell.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { LoginShell } from 'jest-editor-support'; 3 | import { platform } from 'os'; 4 | import * as path from 'path'; 5 | 6 | // based on vscode setting configuration for "terminal.integrated.profiles.osx" 7 | export const LoginShells: Record = { 8 | bash: { 9 | path: 'bash', 10 | args: ['-l'], 11 | }, 12 | zsh: { 13 | path: 'zsh', 14 | args: ['-l'], 15 | }, 16 | fish: { 17 | path: 'fish', 18 | args: ['-l'], 19 | }, 20 | sh: { 21 | path: '/bin/bash', 22 | args: ['-l'], 23 | }, 24 | }; 25 | 26 | export class RunShell { 27 | /** determine toSetting() output; if set to 'never', only the nonLoginShell will ever be returned */ 28 | private _useLoginShell: boolean | 'never'; 29 | private nonLoginShell?: string; 30 | private loginShell?: LoginShell; 31 | 32 | constructor(setting?: string | LoginShell) { 33 | this._useLoginShell = false; 34 | this.initFromSetting(setting); 35 | } 36 | 37 | private initFromSetting(setting?: string | LoginShell): void { 38 | if (setting) { 39 | if (typeof setting === 'string') { 40 | this._useLoginShell = false; 41 | this.nonLoginShell = setting; 42 | this.loginShell = this.getLoginShell(setting); 43 | } else { 44 | if (setting.args?.length > 0) { 45 | this._useLoginShell = true; 46 | this.nonLoginShell = undefined; 47 | this.loginShell = setting; 48 | } else { 49 | this._useLoginShell = false; 50 | this.nonLoginShell = setting.path; 51 | this.loginShell = this.getLoginShell(setting.path); 52 | } 53 | } 54 | } else { 55 | this._useLoginShell = false; 56 | this.nonLoginShell = undefined; 57 | this.loginShell = this.getLoginShell(); 58 | } 59 | if (!this.loginShell) { 60 | this._useLoginShell = 'never'; 61 | } 62 | } 63 | 64 | public get useLoginShell(): boolean | 'never' { 65 | return this._useLoginShell; 66 | } 67 | public enableLoginShell(): void { 68 | if (this._useLoginShell === 'never') { 69 | console.warn('will not enable loginShell for a "never-login-shell" RunShell', this); 70 | return; 71 | } 72 | this._useLoginShell = true; 73 | } 74 | 75 | /** 76 | * returns the shell setting based on useLoginShell flag. If there is no loginShell available, 77 | * for example on windows, the original setting will be returned 78 | */ 79 | public toSetting(): undefined | string | LoginShell { 80 | if (!this._useLoginShell || this._useLoginShell === 'never') { 81 | return this.nonLoginShell; 82 | } 83 | return this.loginShell; 84 | } 85 | 86 | private getLoginShell(shellPath?: string): undefined | LoginShell { 87 | if (platform() === 'win32') { 88 | return; 89 | } 90 | const name = shellPath ? path.parse(shellPath).base : 'sh'; 91 | const shell = LoginShells[name]; 92 | if (!shell) { 93 | const msg = `no login-shell definition found for shell=${name} on ${platform()}`; 94 | console.warn(msg); 95 | vscode.window.showErrorMessage(`${msg}. Please report this issue.`); 96 | } 97 | return shell; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/JestExt/types.ts: -------------------------------------------------------------------------------- 1 | import { JestTotalResults, ProjectWorkspace } from 'jest-editor-support'; 2 | 3 | import * as vscode from 'vscode'; 4 | import { LoggingFactory } from '../logging'; 5 | import { PluginResourceSettings } from '../Settings'; 6 | import { ProcessSession } from './process-session'; 7 | import { JestProcessInfo } from '../JestProcessManagement'; 8 | import { JestOutputTerminal } from './output-terminal'; 9 | import { TestIdentifier } from '../TestResults'; 10 | import { DebugInfo } from '../types'; 11 | 12 | export enum WatchMode { 13 | None = 'none', 14 | Watch = 'watch', 15 | WatchAll = 'watchAll', 16 | } 17 | export interface RunnerWorkspaceOptions { 18 | outputFileSuffix?: string; 19 | collectCoverage?: boolean; 20 | } 21 | export interface JestExtContext { 22 | settings: PluginResourceSettings; 23 | workspace: vscode.WorkspaceFolder; 24 | loggingFactory: LoggingFactory; 25 | createRunnerWorkspace: (options?: RunnerWorkspaceOptions) => ProjectWorkspace; 26 | output: JestOutputTerminal; 27 | } 28 | 29 | export interface JestExtSessionContext extends JestExtContext { 30 | session: ProcessSession; 31 | } 32 | export interface RunEventBase { 33 | process: JestProcessInfo; 34 | } 35 | export type JestRunEvent = RunEventBase & 36 | ( 37 | | { type: 'scheduled' } 38 | | { type: 'data'; text: string; raw?: string; newLine?: boolean; isError?: boolean } 39 | | { type: 'test-error' } 40 | | { type: 'process-start' } 41 | | { type: 'start' } 42 | | { type: 'end'; error?: string } 43 | | { type: 'exit'; error?: string; code?: number } 44 | | { type: 'long-run'; threshold: number; numTotalTestSuites?: number } 45 | ); 46 | 47 | export interface JestTestDataAvailableEvent { 48 | data: JestTotalResults; 49 | process: JestProcessInfo; 50 | } 51 | export interface JestSessionEvents { 52 | onRunEvent: vscode.EventEmitter; 53 | onTestSessionStarted: vscode.EventEmitter; 54 | onTestSessionStopped: vscode.EventEmitter; 55 | onTestDataAvailable: vscode.EventEmitter; 56 | } 57 | export interface JestExtProcessContextRaw extends JestExtContext { 58 | updateWithData: (data: JestTotalResults, process: JestProcessInfo) => void; 59 | onRunEvent: vscode.EventEmitter; 60 | } 61 | export type JestExtProcessContext = Readonly; 62 | 63 | export type DebugTestIdentifier = string | TestIdentifier; 64 | 65 | export type DebugFunction = (debugInfo: DebugInfo) => Promise; 66 | -------------------------------------------------------------------------------- /src/JestProcessManagement/JestProcessManager.ts: -------------------------------------------------------------------------------- 1 | import { JestProcess } from './JestProcess'; 2 | import { 3 | TaskArrayFunctions, 4 | JestProcessRequest, 5 | QueueType, 6 | Task, 7 | JestProcessInfo, 8 | UserDataType, 9 | ProcessStatus, 10 | } from './types'; 11 | import { Logging } from '../logging'; 12 | import { createTaskQueue, TaskQueue } from './task-queue'; 13 | import { isDupe, requestString } from './helper'; 14 | import { JestExtProcessContext } from '../JestExt'; 15 | 16 | export class JestProcessManager implements TaskArrayFunctions { 17 | private extContext: JestExtProcessContext; 18 | private queues: Map>; 19 | private logging: Logging; 20 | 21 | constructor(extContext: JestExtProcessContext) { 22 | this.extContext = extContext; 23 | this.logging = extContext.loggingFactory.create('JestProcessManager'); 24 | this.queues = new Map([ 25 | ['blocking', createTaskQueue('blocking-queue', 1)], 26 | ['blocking-2', createTaskQueue('blocking-queue-2', 1)], 27 | ['non-blocking', createTaskQueue('non-blocking-queue', 3)], 28 | ]); 29 | } 30 | 31 | private getQueue(type: QueueType): TaskQueue { 32 | return this.queues.get(type)!; 33 | } 34 | 35 | private foundDup(request: JestProcessRequest): boolean { 36 | if (!request.schedule.dedupe) { 37 | return false; 38 | } 39 | const queue = this.getQueue(request.schedule.queue); 40 | const dupTasks = queue.filter((p) => isDupe(p, request)); 41 | if (dupTasks.length > 0) { 42 | this.logging( 43 | 'debug', 44 | `found ${dupTasks.length} duplicate processes, will not schedule request:`, 45 | request 46 | ); 47 | return true; 48 | } 49 | return false; 50 | } 51 | /** 52 | * schedule a jest process and handle duplication process if dedupe is requested. 53 | * @param request 54 | * @returns a jest process id if successfully scheduled, otherwise undefined 55 | */ 56 | public scheduleJestProcess( 57 | request: JestProcessRequest, 58 | userData?: UserDataType 59 | ): JestProcessInfo | undefined { 60 | if (this.foundDup(request)) { 61 | this.logging( 62 | 'debug', 63 | `duplicate request found, process is not scheduled: ${requestString(request)}` 64 | ); 65 | return; 66 | } 67 | 68 | const queue = this.getQueue(request.schedule.queue); 69 | const process = new JestProcess(this.extContext, request, userData); 70 | queue.add(process); 71 | this.run(queue); 72 | return process; 73 | } 74 | 75 | // run the first process in the queue 76 | private async run(queue: TaskQueue): Promise { 77 | const task = queue.getRunnableTask(); 78 | if (!task) { 79 | return; 80 | } 81 | const process = task.data; 82 | try { 83 | // process could be cancelled before it starts, so check before starting 84 | if (process.status === ProcessStatus.Pending) { 85 | const promise = process.start(); 86 | this.extContext.onRunEvent.fire({ type: 'process-start', process }); 87 | await promise; 88 | } 89 | } catch (e) { 90 | this.logging('error', `${queue.name}: process failed to start:`, process, e); 91 | this.extContext.onRunEvent.fire({ 92 | type: 'exit', 93 | process, 94 | error: `Process failed to start: ${e}`, 95 | }); 96 | } finally { 97 | queue.remove(task); 98 | } 99 | return this.run(queue); 100 | } 101 | 102 | /** stop and remove all process matching the queue type, if no queue type specified, stop all queues */ 103 | public async stopAll(queueType?: QueueType): Promise { 104 | let promises: Promise[]; 105 | if (!queueType) { 106 | promises = Array.from(this.queues.keys()).map((q) => this.stopAll(q)); 107 | } else { 108 | const queue = this.getQueue(queueType); 109 | promises = queue.map((t) => t.data.stop()); 110 | queue.reset(); 111 | } 112 | await Promise.allSettled(promises); 113 | return; 114 | } 115 | 116 | public numberOfProcesses(queueType?: QueueType): number { 117 | if (queueType) { 118 | return this.getQueue(queueType).size(); 119 | } 120 | return Array.from(this.queues.values()).reduce((pCount, q) => { 121 | pCount += q.size(); 122 | return pCount; 123 | }, 0); 124 | } 125 | 126 | // task array functions 127 | private getQueues(queueType?: QueueType): TaskQueue[] { 128 | return queueType ? [this.getQueue(queueType)] : Array.from(this.queues.values()); 129 | } 130 | public map(f: (task: Task) => M, queueType?: QueueType): M[] { 131 | const queues = this.getQueues(queueType); 132 | return queues.reduce((list, q) => { 133 | list.push(...q.map(f)); 134 | return list; 135 | }, [] as M[]); 136 | } 137 | public filter( 138 | f: (task: Task) => boolean, 139 | queueType?: QueueType 140 | ): Task[] { 141 | const queues = this.getQueues(queueType); 142 | return queues.reduce((list, q) => { 143 | list.push(...q.filter(f)); 144 | return list; 145 | }, [] as Task[]); 146 | } 147 | public find( 148 | f: (task: Task) => boolean, 149 | queueType?: QueueType 150 | ): Task | undefined { 151 | const queues = this.getQueues(queueType); 152 | for (const q of queues) { 153 | const t = q.find(f); 154 | if (t) { 155 | return t; 156 | } 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/JestProcessManagement/helper.ts: -------------------------------------------------------------------------------- 1 | import { JestProcess } from './JestProcess'; 2 | import { JestProcessRequest, Task, TaskPredicate } from './types'; 3 | 4 | export const isRequestEqual = (r1: JestProcessRequest, r2: JestProcessRequest): boolean => { 5 | switch (r1.type) { 6 | case 'by-file': 7 | return r2.type === r1.type && r1.testFileName === r2.testFileName; 8 | case 'by-file-pattern': 9 | return r2.type === r1.type && r1.testFileNamePattern === r2.testFileNamePattern; 10 | case 'by-file-test': 11 | return ( 12 | r2.type === r1.type && 13 | r1.testFileName === r2.testFileName && 14 | r1.testNamePattern === r2.testNamePattern 15 | ); 16 | case 'by-file-test-pattern': 17 | return ( 18 | r2.type === r1.type && 19 | r1.testFileNamePattern === r2.testFileNamePattern && 20 | r1.testNamePattern === r2.testNamePattern 21 | ); 22 | case 'not-test': 23 | return ( 24 | r2.type === r1.type && 25 | r1.args.length === r2.args.length && 26 | r2.args.every((arg) => r1.args.includes(arg)) 27 | ); 28 | default: 29 | return r1.type === r2.type; 30 | } 31 | }; 32 | 33 | export const isDupe = (task: Task, request: JestProcessRequest): boolean => { 34 | const process = task.data; 35 | if (!request.schedule.dedupe) { 36 | return false; 37 | } 38 | const predicate: TaskPredicate = request.schedule.dedupe; 39 | 40 | if (predicate.filterByStatus && !predicate.filterByStatus.includes(task.status)) { 41 | return false; 42 | } 43 | if (predicate.filterByContent !== false && !isRequestEqual(process.request, request)) { 44 | return false; 45 | } 46 | return true; 47 | }; 48 | 49 | const skipAttrs = ['listener', 'run']; 50 | export const requestString = (request: JestProcessRequest): string => { 51 | const replacer = (key: string, value: unknown) => { 52 | if (skipAttrs.includes(key)) { 53 | return typeof value; 54 | } 55 | return value; 56 | }; 57 | return JSON.stringify(request, replacer); 58 | }; 59 | -------------------------------------------------------------------------------- /src/JestProcessManagement/index.ts: -------------------------------------------------------------------------------- 1 | export { JestProcess } from './JestProcess'; 2 | export { JestProcessManager } from './JestProcessManager'; 3 | export { requestString } from './helper'; 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /src/JestProcessManagement/task-queue.ts: -------------------------------------------------------------------------------- 1 | import { TaskArrayFunctions, Task } from './types'; 2 | 3 | export interface TaskQueue extends TaskArrayFunctions { 4 | name: string; 5 | getRunnableTask: () => Task | undefined; 6 | add: (...tasks: T[]) => void; 7 | /** remove specific task or the first task is undefined */ 8 | remove: (...tasks: Task[]) => void; 9 | /** empty the queue */ 10 | reset: () => void; 11 | size: () => number; 12 | } 13 | 14 | /** 15 | * A first-in-first-out queue 16 | * @param name 17 | * @param maxWorker 18 | */ 19 | export const createTaskQueue = (name: string, maxWorker: number): TaskQueue => { 20 | if (maxWorker <= 0) { 21 | throw new Error('invalid maxWorker, should be > 0'); 22 | } 23 | let queue: Task[] = []; 24 | 25 | const toQueueTask = (data: T): Task => ({ data, status: 'pending' }); 26 | 27 | const add = (...data: T[]): void => { 28 | queue.push(...data.map(toQueueTask)); 29 | }; 30 | 31 | const getRunnableTask = () => { 32 | const readyTaskIdx = queue.findIndex((t) => t.status === 'pending'); 33 | if (readyTaskIdx < 0 || readyTaskIdx >= maxWorker) { 34 | return; 35 | } 36 | queue[readyTaskIdx].status = 'running'; 37 | return queue[readyTaskIdx]; 38 | }; 39 | const remove = (...tasks: Task[]) => { 40 | if (tasks.length) { 41 | queue = queue.filter((t) => !tasks.includes(t)); 42 | } else { 43 | queue = queue.slice(1); 44 | } 45 | }; 46 | const map = (f: (task: Task) => M) => queue.map((t) => f(t)); 47 | const filter = (f: (task: Task) => boolean) => queue.filter((t) => f(t)); 48 | const find = (f: (task: Task) => boolean) => queue.find((t) => f(t)); 49 | 50 | return { 51 | name, 52 | add, 53 | remove, 54 | getRunnableTask, 55 | reset: () => (queue.length = 0), 56 | size: (): number => queue.length, 57 | map, 58 | filter, 59 | find, 60 | }; 61 | }; 62 | -------------------------------------------------------------------------------- /src/JestProcessManagement/types.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { RunnerEvent } from 'jest-editor-support'; 3 | import { JestTestProcessType } from '../Settings'; 4 | import { JestProcess } from './JestProcess'; 5 | import { JestTestRun } from '../test-provider/jest-test-run'; 6 | import { TestNamePattern } from '../types'; 7 | 8 | export interface JestProcessListener { 9 | onEvent: (process: JestProcess, event: RunnerEvent, ...args: unknown[]) => unknown; 10 | } 11 | export type JestProcessStatus = 'pending' | 'running' | 'stopping' | 'stopped'; 12 | export interface UserDataType { 13 | run?: JestTestRun; 14 | execError?: boolean; 15 | testError?: boolean; 16 | testItem?: vscode.TestItem; 17 | } 18 | export enum ProcessStatus { 19 | Pending = 'pending', 20 | Running = 'running', 21 | Cancelled = 'cancelled', 22 | // process exited not because of cancellation 23 | Done = 'done', 24 | } 25 | 26 | export interface JestProcessInfo { 27 | readonly id: string; 28 | readonly request: JestProcessRequest; 29 | // user data is a way to store data that is outside of the process managed by the processManager. 30 | // subsequent use of this data is up to the user but should be aware that multiple components might contribute to this data. 31 | userData?: UserDataType; 32 | stop: () => Promise; 33 | status: ProcessStatus; 34 | isWatchMode: boolean; 35 | // starting a timer to automatically kill the process after x milliseconds if the process is still running. 36 | autoStop: (delay?: number, onStop?: (process: JestProcessInfo) => void) => void; 37 | } 38 | 39 | export type TaskStatus = 'running' | 'pending'; 40 | export interface Task { 41 | data: T; 42 | status: TaskStatus; 43 | } 44 | 45 | export type QueueType = 'blocking' | 'blocking-2' | 'non-blocking'; 46 | 47 | /** 48 | * predicate to match task 49 | * @param filterByStatus filter by task status, if omit then any status will be matched. If omit, default is matching any status 50 | * @param filterByContent if to match by all property of the process request. If omit, default is true 51 | */ 52 | export interface TaskPredicate { 53 | filterByStatus?: TaskStatus[]; 54 | filterByContent?: boolean; 55 | } 56 | 57 | type JestProcessTestRequestBase = 58 | | { 59 | type: Extract; 60 | } 61 | | { 62 | type: Extract; 63 | updateSnapshot?: boolean; 64 | nonBlocking?: boolean; 65 | } 66 | | { 67 | type: Extract; 68 | testFileName: string; 69 | updateSnapshot?: boolean; 70 | notTestFile?: boolean; 71 | } 72 | | { 73 | type: Extract; 74 | testFileName: string; 75 | testNamePattern: TestNamePattern; 76 | updateSnapshot?: boolean; 77 | } 78 | | { 79 | type: Extract; 80 | testFileNamePattern: string; 81 | updateSnapshot?: boolean; 82 | } 83 | | { 84 | type: Extract; 85 | testFileNamePattern: string; 86 | testNamePattern: TestNamePattern; 87 | updateSnapshot?: boolean; 88 | }; 89 | 90 | type JestProcessTestRequestCommon = { 91 | coverage?: boolean; 92 | }; 93 | 94 | export type JestProcessTestRequestType = JestProcessTestRequestCommon & JestProcessTestRequestBase; 95 | 96 | type JestProcessNonTestRequest = { 97 | type: Extract; 98 | args: string[]; 99 | }; 100 | 101 | type JestProcessRequestType = JestProcessTestRequestType | JestProcessNonTestRequest; 102 | 103 | /** 104 | * define the eligibility for process scheduling 105 | * @param queue the type of the queue 106 | * @param dedupe a predicate to match the task in queue. 107 | */ 108 | export interface ScheduleStrategy { 109 | queue: QueueType; 110 | dedupe?: TaskPredicate; 111 | } 112 | 113 | interface JestProcessRequestCommon { 114 | schedule: ScheduleStrategy; 115 | listener: JestProcessListener; 116 | } 117 | export type JestProcessRequestTransform = (request: JestProcessRequest) => JestProcessRequest; 118 | 119 | export type JestProcessRequestBase = JestProcessRequestType & { 120 | transform?: JestProcessRequestTransform; 121 | }; 122 | export type JestProcessRequest = JestProcessRequestBase & JestProcessRequestCommon; 123 | 124 | export interface TaskArrayFunctions { 125 | map: (f: (task: Task) => M) => M[]; 126 | filter: (f: (task: Task) => boolean) => Task[]; 127 | find: (f: (task: Task) => boolean) => Task | undefined; 128 | } 129 | -------------------------------------------------------------------------------- /src/Settings/helper.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { 3 | GetConfigFunction, 4 | VirtualFolderSettings, 5 | VirtualFolderSettingKey, 6 | SettingDetail, 7 | } from './types'; 8 | import { isVirtualWorkspaceFolder } from '../virtual-workspace-folder'; 9 | 10 | /** 11 | * Returns a function that retrieves Jest configuration settings for a given workspace folder. 12 | * If the workspace folder is a virtual folder, it first checks for the corresponding virtual folder setting. 13 | * If the setting is not found, it falls back to the workspace setting. 14 | * @param workspaceFolder The workspace folder to retrieve Jest configuration settings for. 15 | * @returns A function that takes a `VirtualFolderSettingKey` as an argument and returns the corresponding setting value. 16 | */ 17 | export const createJestSettingGetter = ( 18 | workspaceFolder: vscode.WorkspaceFolder 19 | ): GetConfigFunction => { 20 | const config = vscode.workspace.getConfiguration('jest', workspaceFolder.uri); 21 | let vFolder: VirtualFolderSettings | undefined; 22 | 23 | if (isVirtualWorkspaceFolder(workspaceFolder)) { 24 | const virtualFolders = config.get('virtualFolders'); 25 | vFolder = virtualFolders?.find((v) => v.name === workspaceFolder.name); 26 | if (!vFolder) { 27 | throw new Error(`[${workspaceFolder.name}] is missing corresponding virtual folder setting`); 28 | } 29 | } 30 | 31 | // get setting from virtual folder first, fallback to workspace setting if not found 32 | const getSetting = (key: VirtualFolderSettingKey): T | undefined => { 33 | if (key === 'enable') { 34 | // if any of the folders is disabled, then the whole workspace is disabled 35 | return (config.get(key) !== false && vFolder?.enable !== false) as T; 36 | } 37 | 38 | return (vFolder?.[key] as T) ?? config.get(key); 39 | }; 40 | return getSetting; 41 | }; 42 | 43 | // get setting from virtual folder first, fallback to workspace setting if not found 44 | export const updateSetting = async ( 45 | workspaceFolder: vscode.WorkspaceFolder, 46 | key: VirtualFolderSettingKey, 47 | value: unknown 48 | ): Promise => { 49 | const config = vscode.workspace.getConfiguration('jest', workspaceFolder.uri); 50 | if (!isVirtualWorkspaceFolder(workspaceFolder)) { 51 | await config.update(key, value); 52 | return; 53 | } 54 | const virtualFolders = config.get('virtualFolders'); 55 | const vFolder = virtualFolders?.find((v) => v.name === workspaceFolder.name); 56 | if (!vFolder) { 57 | throw new Error(`[${workspaceFolder.name}] is missing corresponding virtual folder setting`); 58 | } 59 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 60 | (vFolder as any)[key] = value; 61 | await config.update('virtualFolders', virtualFolders); 62 | }; 63 | 64 | export const getSettingDetail = (configName: string, section: string): SettingDetail => { 65 | // Use the `inspect` method to get detailed information about the setting 66 | const config = vscode.workspace.getConfiguration(configName); 67 | const value = config.get(section); 68 | const settingInspection = config.inspect(section); 69 | 70 | if (settingInspection) { 71 | const isExplicitlySet = 72 | settingInspection.globalValue !== undefined || 73 | settingInspection.workspaceValue !== undefined || 74 | settingInspection.workspaceFolderValue !== undefined || 75 | settingInspection.globalLanguageValue !== undefined || 76 | settingInspection.workspaceLanguageValue !== undefined || 77 | settingInspection.workspaceFolderLanguageValue !== undefined; 78 | 79 | return { value, isExplicitlySet }; 80 | } 81 | return { value: undefined, isExplicitlySet: false }; 82 | }; 83 | -------------------------------------------------------------------------------- /src/Settings/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './helper'; 3 | -------------------------------------------------------------------------------- /src/Settings/types.ts: -------------------------------------------------------------------------------- 1 | import { CoverageColors } from '../Coverage/CoverageOverlay'; 2 | import { JESParserPluginOptions, ProjectWorkspace } from 'jest-editor-support'; 3 | import { RunShell } from '../JestExt/run-shell'; 4 | import { RunMode } from '../JestExt/run-mode'; 5 | 6 | export type JestTestProcessType = 7 | | 'all-tests' 8 | | 'watch-tests' 9 | | 'watch-all-tests' 10 | | 'by-file' 11 | | 'by-file-test' 12 | | 'not-test' 13 | | 'by-file-test-pattern' 14 | | 'by-file-pattern'; 15 | 16 | export type OnStartupType = Extract[]; 17 | export type OnSaveFileType = 'test-file' | 'test-src-file'; 18 | export type JestExtAutoRunShortHand = 'default' | 'watch' | 'on-save' | 'legacy' | 'off'; 19 | 20 | export type JestExtAutoRunConfig = 21 | | { watch: true; onStartup?: OnStartupType } 22 | | { 23 | watch: false; 24 | onStartup?: OnStartupType; 25 | onSave?: OnSaveFileType; 26 | }; 27 | export type JestExtAutoRunSetting = JestExtAutoRunShortHand | JestExtAutoRunConfig; 28 | 29 | export interface JestRunModeOptions { 30 | runAllTestsOnStartup?: boolean; 31 | coverage?: boolean; 32 | deferred?: boolean; 33 | 34 | // TestExplorer related settings 35 | showInlineError?: boolean; 36 | } 37 | export type JestRunMode = ( 38 | | { type: 'watch' } 39 | | { type: 'on-demand' } 40 | | { type: 'on-save'; testFileOnly?: boolean } 41 | ) & 42 | JestRunModeOptions; 43 | 44 | export type JestRunModeType = JestRunMode['type']; 45 | export type JestPredefinedRunModeType = JestRunModeType | 'deferred'; 46 | export type JestRunModeSetting = JestRunMode | JestPredefinedRunModeType; 47 | 48 | export interface JestRawOutputSetting { 49 | revealWithFocus?: 'terminal' | 'test-results' | 'none'; 50 | revealOn?: 'run' | 'error' | 'demand'; 51 | clearOnRun?: 'both' | 'terminal' | 'test-results' | 'none'; 52 | } 53 | export type JestPredefinedOutputSetting = 'neutral' | 'terminal-based' | 'test-results-based'; 54 | export type JestOutputSetting = JestPredefinedOutputSetting | JestRawOutputSetting; 55 | 56 | export type TestExplorerConfigLegacy = 57 | | { enabled: false } 58 | | { enabled: true; showClassicStatus?: boolean; showInlineError?: boolean }; 59 | 60 | export interface TestExplorerConfig { 61 | showInlineError?: boolean; 62 | } 63 | 64 | export type NodeEnv = ProjectWorkspace['nodeEnv']; 65 | export type MonitorLongRun = 'off' | number; 66 | export type AutoRevealOutputType = 'on-run' | 'on-exec-error' | 'off'; 67 | export interface PluginResourceSettings { 68 | jestCommandLine?: string; 69 | rootPath: string; 70 | coverageFormatter: string; 71 | debugMode?: boolean; 72 | coverageColors?: CoverageColors; 73 | runMode: RunMode; 74 | nodeEnv?: NodeEnv; 75 | shell: RunShell; 76 | monitorLongRun?: MonitorLongRun; 77 | enable?: boolean; 78 | parserPluginOptions?: JESParserPluginOptions; 79 | useDashedArgs?: boolean; 80 | useJest30?: boolean; 81 | } 82 | 83 | export interface DeprecatedPluginResourceSettings { 84 | showCoverageOnLoad?: boolean; 85 | autoRun?: JestExtAutoRunSetting | null; 86 | testExplorer?: TestExplorerConfig; 87 | } 88 | 89 | export interface PluginWindowSettings { 90 | disabledWorkspaceFolders: string[]; 91 | } 92 | 93 | export type AllPluginResourceSettings = PluginResourceSettings & DeprecatedPluginResourceSettings; 94 | 95 | export type VirtualFolderSettingKey = keyof AllPluginResourceSettings; 96 | export interface VirtualFolderSettings extends AllPluginResourceSettings { 97 | name: string; 98 | } 99 | 100 | export type GetConfigFunction = (key: VirtualFolderSettingKey) => T | undefined; 101 | 102 | export interface SettingDetail { 103 | value: T | undefined; 104 | /** true if the setting is explicitly defined in a settings file, i.e., not from default value */ 105 | isExplicitlySet: boolean; 106 | } 107 | -------------------------------------------------------------------------------- /src/TestResults/TestResult.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JestFileResults, 3 | JestTotalResults, 4 | CodeLocation as Location, 5 | TestReconciliationState, 6 | } from 'jest-editor-support'; 7 | import { CoverageMapData, FileCoverageData } from 'istanbul-lib-coverage'; 8 | import * as path from 'path'; 9 | import { cleanAnsi, toLowerCaseDriveLetter } from '../helpers'; 10 | import { MatchEvent } from './match-node'; 11 | 12 | export interface LocationRange { 13 | start: Location; 14 | end: Location; 15 | } 16 | 17 | export interface TestIdentifier { 18 | title: string; 19 | ancestorTitles: string[]; 20 | } 21 | 22 | export interface TestResult extends LocationRange { 23 | name: string; 24 | 25 | identifier: TestIdentifier; 26 | 27 | status: TestReconciliationState; 28 | shortMessage?: string; 29 | terseMessage?: string; 30 | 31 | /** Zero-based line number */ 32 | lineNumberOfError?: number; 33 | 34 | // multiple results for the given range, common for parameterized (.each) tests 35 | multiResults?: TestResult[]; 36 | 37 | // matching process history 38 | sourceHistory?: MatchEvent[]; 39 | assertionHistory?: MatchEvent[]; 40 | } 41 | 42 | function testResultWithLowerCaseWindowsDriveLetter(testResult: JestFileResults): JestFileResults { 43 | const newFilePath = toLowerCaseDriveLetter(testResult.name); 44 | if (newFilePath) { 45 | return { 46 | ...testResult, 47 | name: newFilePath, 48 | }; 49 | } 50 | 51 | return testResult; 52 | } 53 | 54 | export const testResultsWithLowerCaseWindowsDriveLetters = ( 55 | testResults: JestFileResults[] 56 | ): JestFileResults[] => { 57 | if (!testResults) { 58 | return testResults; 59 | } 60 | 61 | return testResults.map(testResultWithLowerCaseWindowsDriveLetter); 62 | }; 63 | 64 | function fileCoverageWithLowerCaseWindowsDriveLetter( 65 | fileCoverage: FileCoverageData 66 | ): FileCoverageData { 67 | const newFilePath = toLowerCaseDriveLetter(fileCoverage.path); 68 | if (newFilePath) { 69 | return { 70 | ...fileCoverage, 71 | path: newFilePath, 72 | }; 73 | } 74 | 75 | return fileCoverage; 76 | } 77 | 78 | export const coverageMapWithLowerCaseWindowsDriveLetters = ( 79 | data: JestTotalResults 80 | ): CoverageMapData | undefined => { 81 | if (!data.coverageMap) { 82 | return; 83 | } 84 | 85 | const result: CoverageMapData = {}; 86 | const filePaths = Object.keys(data.coverageMap); 87 | 88 | for (const filePath of filePaths) { 89 | const newFileCoverage = fileCoverageWithLowerCaseWindowsDriveLetter(data.coverageMap[filePath]); 90 | result[newFileCoverage.path] = newFileCoverage; 91 | } 92 | 93 | return result; 94 | }; 95 | 96 | /** 97 | * Normalize file paths on Windows systems to use lowercase drive letters. 98 | * This follows the standard used by Visual Studio Code for URIs which includes 99 | * the document fileName property. 100 | * 101 | * @param data Parsed JSON results 102 | */ 103 | export const resultsWithLowerCaseWindowsDriveLetters = ( 104 | data: JestTotalResults 105 | ): JestTotalResults => { 106 | if (path.sep === '\\') { 107 | return { 108 | ...data, 109 | coverageMap: coverageMapWithLowerCaseWindowsDriveLetters(data), 110 | testResults: testResultsWithLowerCaseWindowsDriveLetters(data.testResults), 111 | }; 112 | } 113 | 114 | return data; 115 | }; 116 | 117 | /** 118 | * Removes ANSI escape sequence characters from test results in order to get clean messages 119 | */ 120 | export const resultsWithoutAnsiEscapeSequence = (data: JestTotalResults): JestTotalResults => { 121 | if (!data || !data.testResults) { 122 | return data; 123 | } 124 | 125 | return { 126 | ...data, 127 | testResults: data.testResults.map((result) => ({ 128 | ...result, 129 | message: cleanAnsi(result.message), 130 | assertionResults: result.assertionResults.map((assertion) => ({ 131 | ...assertion, 132 | failureMessages: (assertion.failureMessages ?? []).map((message) => cleanAnsi(message)), 133 | })), 134 | })), 135 | }; 136 | }; 137 | 138 | // enum based on TestReconciliationState 139 | export const TestStatus: { 140 | [key in TestReconciliationState]: TestReconciliationState; 141 | } = { 142 | Unknown: 'Unknown', 143 | KnownSuccess: 'KnownSuccess', 144 | KnownFail: 'KnownFail', 145 | KnownSkip: 'KnownSkip', 146 | KnownTodo: 'KnownTodo', 147 | }; 148 | 149 | // export type StatusInfo = {[key in TestReconciliationState]: T}; 150 | export interface StatusInfo { 151 | precedence: number; 152 | desc: string; 153 | } 154 | 155 | export const TestResultStatusInfo: { [key in TestReconciliationState]: StatusInfo } = { 156 | KnownFail: { precedence: 1, desc: 'Failed' }, 157 | Unknown: { 158 | precedence: 2, 159 | desc: 'Test has not run yet, due to Jest only running tests related to changes.', 160 | }, 161 | KnownSkip: { precedence: 3, desc: 'Skipped' }, 162 | KnownSuccess: { precedence: 4, desc: 'Passed' }, 163 | KnownTodo: { precedence: 5, desc: 'Todo' }, 164 | }; 165 | -------------------------------------------------------------------------------- /src/TestResults/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | TestResult, 3 | resultsWithLowerCaseWindowsDriveLetters, 4 | TestResultStatusInfo, 5 | TestIdentifier, 6 | TestStatus, 7 | } from './TestResult'; 8 | export * from './TestResultProvider'; 9 | -------------------------------------------------------------------------------- /src/TestResults/snapshot-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Snapshot, SnapshotBlock, SnapshotParserOptions } from 'jest-editor-support'; 3 | import { escapeRegExp } from '../helpers'; 4 | 5 | export type SnapshotStatus = 'exists' | 'missing' | 'inline'; 6 | 7 | export interface ExtSnapshotBlock extends SnapshotBlock { 8 | isInline: boolean; 9 | } 10 | export interface SnapshotSuite { 11 | testPath: string; 12 | blocks: ExtSnapshotBlock[]; 13 | } 14 | 15 | const inlineKeys = ['toMatchInlineSnapshot', 'toThrowErrorMatchingInlineSnapshot']; 16 | export class SnapshotProvider { 17 | private snapshotSupport: Snapshot; 18 | private panel?: vscode.WebviewPanel; 19 | 20 | constructor() { 21 | this.snapshotSupport = new Snapshot(undefined, inlineKeys); 22 | } 23 | 24 | public parse(testPath: string, options?: SnapshotParserOptions): SnapshotSuite { 25 | try { 26 | const sBlocks = this.snapshotSupport.parse(testPath, options); 27 | const blocks = sBlocks.map((block) => ({ 28 | ...block, 29 | isInline: inlineKeys.find((key) => block.node.name.includes(key)) ? true : false, 30 | })); 31 | const snapshotSuite = { testPath, blocks }; 32 | return snapshotSuite; 33 | } catch (e) { 34 | console.warn('[SnapshotProvider] parse failed:', e); 35 | return { testPath, blocks: [] }; 36 | } 37 | } 38 | 39 | private escapeContent = (content: string) => { 40 | if (content) { 41 | const escaped = content 42 | .replace(/&/g, '&') 43 | .replace(/"/g, '"') 44 | .replace(/'/g, ''') 45 | .replace(//g, '>'); 47 | return `
${escaped}
`; 48 | } 49 | }; 50 | public async previewSnapshot(testPath: string, testFullName: string): Promise { 51 | const content = await this.snapshotSupport.getSnapshotContent( 52 | testPath, 53 | new RegExp(`^${escapeRegExp({ value: testFullName, exactMatch: false })} [0-9]+$`) 54 | ); 55 | const noSnapshotFound = (): void => { 56 | vscode.window.showErrorMessage('no snapshot is found, please run test to generate first'); 57 | return; 58 | }; 59 | if (!content) { 60 | return noSnapshotFound(); 61 | } 62 | let contentString: string | undefined; 63 | if (typeof content === 'string') { 64 | contentString = this.escapeContent(content); 65 | } else { 66 | const entries = Object.entries(content); 67 | switch (entries.length) { 68 | case 0: 69 | return noSnapshotFound(); 70 | case 1: 71 | contentString = this.escapeContent(entries[0][1]); 72 | break; 73 | default: { 74 | const strings = entries.map( 75 | ([key, value]) => `

${key}

${this.escapeContent(value)}` 76 | ); 77 | contentString = strings.join('
'); 78 | break; 79 | } 80 | } 81 | } 82 | 83 | if (this.panel) { 84 | this.panel.reveal(); 85 | } else { 86 | this.panel = vscode.window.createWebviewPanel( 87 | 'view_snapshot', 88 | testFullName, 89 | vscode.ViewColumn.Two, 90 | {} 91 | ); 92 | 93 | this.panel.onDidDispose(() => { 94 | this.panel = undefined; 95 | }); 96 | } 97 | 98 | this.panel.webview.html = contentString ?? ''; 99 | this.panel.title = testFullName; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/TestResults/test-result-events.ts: -------------------------------------------------------------------------------- 1 | import { ItBlock } from 'jest-editor-support'; 2 | import * as vscode from 'vscode'; 3 | import { JestProcessInfo } from '../JestProcessManagement'; 4 | import { ContainerNode } from './match-node'; 5 | 6 | export type TestSuiteChangeReason = 'assertions-updated' | 'result-matched'; 7 | export type TestSuitChangeEvent = 8 | | { 9 | type: 'assertions-updated'; 10 | process: JestProcessInfo; 11 | files: string[]; 12 | } 13 | | { 14 | type: 'result-matched'; 15 | file: string; 16 | } 17 | | { 18 | type: 'result-match-failed'; 19 | file: string; 20 | sourceContainer: ContainerNode; 21 | }; 22 | 23 | export const createTestResultEvents = () => ({ 24 | testListUpdated: new vscode.EventEmitter(), 25 | testSuiteChanged: new vscode.EventEmitter(), 26 | }); 27 | export type TestResultEvents = ReturnType; 28 | -------------------------------------------------------------------------------- /src/appGlobals.ts: -------------------------------------------------------------------------------- 1 | export const extensionName = 'io.orta.jest'; 2 | export const extensionId = 'orta.vscode-jest'; 3 | export const SupportedLanguageIds = [ 4 | 'javascript', 5 | 'javascriptreact', 6 | 'typescript', 7 | 'typescriptreact', 8 | 'vue', 9 | ]; 10 | 11 | export const REPO_BASE_URL = 'https://github.com/jest-community/vscode-jest'; 12 | export const TROUBLESHOOTING_URL = `${REPO_BASE_URL}#troubleshooting`; 13 | export const LONG_RUN_TROUBLESHOOTING_URL = `${REPO_BASE_URL}#what-to-do-with-long-running-tests-warning`; 14 | export const OUTPUT_CONFIG_HELP_URL = `${REPO_BASE_URL}#outputconfig`; 15 | -------------------------------------------------------------------------------- /src/diagnostics.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * this module contains functions to show jest test results in 3 | * vscode inspector via the DiagnosticsCollection. 4 | */ 5 | import * as vscode from 'vscode'; 6 | import { existsSync } from 'fs'; 7 | import { TestFileAssertionStatus } from 'jest-editor-support'; 8 | import { TestStatus, TestResult } from './TestResults'; 9 | import { testIdString } from './helpers'; 10 | 11 | function createDiagnosticWithRange( 12 | message: string, 13 | range: vscode.Range, 14 | testName?: string 15 | ): vscode.Diagnostic { 16 | const msg = testName ? `${testName}\n-----\n${message}` : message; 17 | const diag = new vscode.Diagnostic(range, msg, vscode.DiagnosticSeverity.Error); 18 | diag.source = 'Jest'; 19 | return diag; 20 | } 21 | 22 | function createDiagnostic( 23 | message: string, 24 | lineNumber: number, 25 | name?: string, 26 | startCol = 0, 27 | endCol = Number.MAX_SAFE_INTEGER 28 | ): vscode.Diagnostic { 29 | const line = lineNumber > 0 ? lineNumber - 1 : 0; 30 | return createDiagnosticWithRange(message, new vscode.Range(line, startCol, line, endCol), name); 31 | } 32 | 33 | // update diagnostics for the active editor 34 | // it will utilize the parsed test result to mark actual text position. 35 | export function updateCurrentDiagnostics( 36 | testResults: TestResult[], 37 | collection: vscode.DiagnosticCollection, 38 | editor: vscode.TextEditor 39 | ): void { 40 | const uri = editor.document.uri; 41 | 42 | if (!testResults.length) { 43 | collection.delete(uri); 44 | return; 45 | } 46 | const allDiagnostics = testResults.reduce((list, tr) => { 47 | const allResults = tr.multiResults ? [tr, ...tr.multiResults] : [tr]; 48 | const diagnostics = allResults 49 | .filter((r) => r.status === TestStatus.KnownFail) 50 | .map((r) => { 51 | const line = r.lineNumberOfError || r.end.line; 52 | const textLine = editor.document.lineAt(line); 53 | const name = testIdString('display', r.identifier); 54 | return createDiagnosticWithRange( 55 | r.shortMessage || r.terseMessage || 'unknown error', 56 | textLine.range, 57 | name 58 | ); 59 | }); 60 | list.push(...diagnostics); 61 | return list; 62 | }, [] as vscode.Diagnostic[]); 63 | 64 | collection.set(uri, allDiagnostics); 65 | } 66 | 67 | // update all diagnosis with jest test results 68 | // note, this method aim to quickly lay down the diagnosis baseline. 69 | // For performance reason, we will not parse individual file here, therefore 70 | // will not have the actual info about text position. However when the file 71 | // become active, it will then utilize the actual file content via updateCurrentDiagnostics() 72 | 73 | export function updateDiagnostics( 74 | testResults: TestFileAssertionStatus[], 75 | collection: vscode.DiagnosticCollection 76 | ): void { 77 | function addTestFileError(result: TestFileAssertionStatus, uri: vscode.Uri): void { 78 | const diag = createDiagnostic(result.message || 'test file error', 0, undefined, 0, 0); 79 | collection.set(uri, [diag]); 80 | } 81 | 82 | function addTestsError(result: TestFileAssertionStatus, uri: vscode.Uri): void { 83 | if (!result.assertions) { 84 | return; 85 | } 86 | const asserts = result.assertions.filter((a) => a.status === TestStatus.KnownFail); 87 | collection.set( 88 | uri, 89 | asserts.map((assertion) => { 90 | const name = testIdString('display', assertion); 91 | return createDiagnostic( 92 | assertion.shortMessage || assertion.message, 93 | assertion.line ?? -1, 94 | name 95 | ); 96 | }) 97 | ); 98 | } 99 | 100 | testResults.forEach((result) => { 101 | const uri = vscode.Uri.file(result.file); 102 | switch (result.status) { 103 | case TestStatus.KnownFail: 104 | if (result.assertions && result.assertions.length <= 0) { 105 | addTestFileError(result, uri); 106 | } else { 107 | addTestsError(result, uri); 108 | } 109 | break; 110 | default: 111 | collection.delete(uri); 112 | break; 113 | } 114 | }); 115 | 116 | // Remove diagnostics for files no longer in existence 117 | const toBeDeleted: vscode.Uri[] = []; 118 | collection.forEach((uri) => { 119 | if (!existsSync(uri.fsPath)) { 120 | toBeDeleted.push(uri); 121 | } 122 | }); 123 | toBeDeleted.forEach((uri) => { 124 | collection.delete(uri); 125 | }); 126 | } 127 | 128 | export function resetDiagnostics(diagnostics: vscode.DiagnosticCollection): void { 129 | diagnostics.clear(); 130 | } 131 | export function failedSuiteCount(diagnostics: vscode.DiagnosticCollection): number { 132 | let sum = 0; 133 | diagnostics.forEach(() => sum++); 134 | return sum; 135 | } 136 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | export interface ExtErrorDef { 2 | code: number; 3 | type: 'error' | 'warn'; 4 | desc: string; 5 | helpLink: string; 6 | } 7 | 8 | const BASE_URL = 'https://github.com/jest-community/vscode-jest/blob/master/README.md'; 9 | export const GENERIC_ERROR: ExtErrorDef = { 10 | code: 1, 11 | type: 'error', 12 | desc: 'jest test run failed', 13 | helpLink: `${BASE_URL}#troubleshooting`, 14 | }; 15 | export const CMD_NOT_FOUND: ExtErrorDef = { 16 | code: 2, 17 | type: 'error', 18 | desc: 'jest process failed to start, most likely due to env or project configuration issues', 19 | helpLink: `${BASE_URL}#jest-failed-to-run`, 20 | }; 21 | export const LONG_RUNNING_TESTS: ExtErrorDef = { 22 | code: 3, 23 | type: 'warn', 24 | desc: 'jest test run exceed the configured threshold ("jest.monitorLongRun") ', 25 | helpLink: `${BASE_URL}#what-to-do-with-long-running-tests-warning`, 26 | }; 27 | 28 | export const getExitErrorDef = (exitCode?: number): ExtErrorDef | undefined => { 29 | if (exitCode === 127) { 30 | return CMD_NOT_FOUND; 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | import { statusBar } from './StatusBar'; 4 | import { ExtensionManager } from './extension-manager'; 5 | import { tiContextManager } from './test-provider/test-item-context-manager'; 6 | import * as languageProvider from './language-provider'; 7 | import { noOpFileSystemProvider } from './noop-fs-provider'; 8 | import { executableTerminalLinkProvider } from './terminal-link-provider'; 9 | import { outputManager } from './output-manager'; 10 | 11 | let extensionManager: ExtensionManager; 12 | 13 | const addSubscriptions = (context: vscode.ExtensionContext): void => { 14 | const languages = [ 15 | { language: 'javascript' }, 16 | { language: 'javascriptreact' }, 17 | { language: 'typescript' }, 18 | { language: 'typescriptreact' }, 19 | { language: 'vue' }, 20 | ]; 21 | 22 | // command function 23 | 24 | context.subscriptions.push( 25 | ...statusBar.register((folder: string) => extensionManager.getByName(folder)), 26 | ...extensionManager.register(), 27 | vscode.languages.registerCodeLensProvider(languages, extensionManager.coverageCodeLensProvider), 28 | ...tiContextManager.registerCommands(), 29 | ...languageProvider.register(), 30 | noOpFileSystemProvider.register(), 31 | executableTerminalLinkProvider.register(), 32 | ...outputManager.register() 33 | ); 34 | }; 35 | 36 | export function activate(context: vscode.ExtensionContext): void { 37 | extensionManager = new ExtensionManager(context); 38 | addSubscriptions(context); 39 | extensionManager.activate(); 40 | } 41 | export function deactivate(): void { 42 | extensionManager.deleteAllExtensions(); 43 | } 44 | -------------------------------------------------------------------------------- /src/language-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | 4 | const JestMockModuleApi = 5 | /jest\.(mock|unmock|domock|dontmock|setmock|requireActual|requireMock|createMockFromModule|unstable_mockModule)\(['"](.*)/gi; 6 | const ImportFileRegex = /^([^\\.].*)\.(json|jsx|tsx|mjs|cjs|js|ts)$/gi; 7 | 8 | const toCompletionItem = ( 9 | label: string, 10 | kind = vscode.CompletionItemKind.File, 11 | detail?: string 12 | ): vscode.CompletionItem => { 13 | const cItem = new vscode.CompletionItem(label, kind); 14 | cItem.detail = detail ?? label; 15 | return cItem; 16 | }; 17 | 18 | /** 19 | * auto complete path-based parameter for jest module-related methods 20 | */ 21 | export class LocalFileCompletionItemProvider 22 | implements vscode.CompletionItemProvider 23 | { 24 | public async provideCompletionItems( 25 | document: vscode.TextDocument, 26 | position: vscode.Position 27 | ): Promise { 28 | const linePrefix = document.lineAt(position).text.slice(0, position.character); 29 | const matched = [...linePrefix.matchAll(JestMockModuleApi)][0]; 30 | if (!matched) { 31 | return undefined; 32 | } 33 | const userInput: string = Array.from(matched)[2]; 34 | const documentDir = path.dirname(document.uri.fsPath); 35 | const targetDir = path.resolve(documentDir, userInput); 36 | 37 | const results = await vscode.workspace.fs.readDirectory(vscode.Uri.file(targetDir)); 38 | 39 | const items: vscode.CompletionItem[] = []; 40 | results.forEach(([p, fType]) => { 41 | if (fType === vscode.FileType.Directory) { 42 | items.push(toCompletionItem(p, vscode.CompletionItemKind.Folder)); 43 | } else if (fType === vscode.FileType.File) { 44 | const matched = [...p.matchAll(ImportFileRegex)][0]; 45 | if (matched) { 46 | const [, module, ext] = matched; 47 | if (ext === 'json') { 48 | items.push(toCompletionItem(p)); 49 | } else { 50 | items.push(toCompletionItem(module, vscode.CompletionItemKind.File, p)); 51 | } 52 | } 53 | } 54 | }); 55 | return items; 56 | } 57 | } 58 | 59 | export function register(): vscode.Disposable[] { 60 | const selector = [ 61 | { scheme: 'file', language: 'javascript' }, 62 | { scheme: 'file', language: 'javascriptreact' }, 63 | { scheme: 'file', language: 'typescript' }, 64 | { scheme: 'file', language: 'typescriptreact' }, 65 | ]; 66 | return [ 67 | vscode.languages.registerCompletionItemProvider( 68 | selector, 69 | new LocalFileCompletionItemProvider(), 70 | '/' 71 | ), 72 | ]; 73 | } 74 | -------------------------------------------------------------------------------- /src/logging.ts: -------------------------------------------------------------------------------- 1 | export type LoggingType = 'debug' | 'error' | 'warn'; 2 | export type Logging = (type: LoggingType, ...args: unknown[]) => void; 3 | export interface LoggingFactory { 4 | create: (id: string) => Logging; 5 | } 6 | 7 | export const workspaceLogging = (workspaceName: string, verbose: boolean): LoggingFactory => { 8 | const create = 9 | (id: string): Logging => 10 | (type: LoggingType, ...args: unknown[]): void => { 11 | const name = `[${workspaceName}/${id}]`; 12 | if (type === 'debug') { 13 | if (verbose) { 14 | console.log(name, ...args); 15 | } 16 | return; 17 | } 18 | if (type === 'warn') { 19 | console.warn(name, ...args); 20 | return; 21 | } 22 | 23 | console.error(name, ...args); 24 | }; 25 | return { create }; 26 | }; 27 | -------------------------------------------------------------------------------- /src/noop-fs-provider.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | 3 | import * as vscode from 'vscode'; 4 | 5 | /** 6 | * This class is a dummy file system provider, which is used to silence the default file system provider 7 | * behavior, such as prompting user to save the file for untitled file. 8 | */ 9 | 10 | export class NoOpFileSystemProvider implements vscode.FileSystemProvider { 11 | public static scheme = `vscode-jest-noop`; 12 | private _onDidChangeFile: vscode.EventEmitter = 13 | new vscode.EventEmitter(); 14 | readonly onDidChangeFile: vscode.Event = this._onDidChangeFile.event; 15 | 16 | // All methods are no-ops 17 | readFile(): Uint8Array { 18 | return new Uint8Array(); 19 | } 20 | writeFile(): void {} 21 | watch(): vscode.Disposable { 22 | return new vscode.Disposable(() => {}); 23 | } 24 | stat(): vscode.FileStat { 25 | return { type: vscode.FileType.File, ctime: 0, mtime: 0, size: 0 }; 26 | } 27 | readDirectory(): [string, vscode.FileType][] { 28 | return []; 29 | } 30 | createDirectory(): void {} 31 | delete(): void {} 32 | rename(): void {} 33 | register(): vscode.Disposable { 34 | return vscode.workspace.registerFileSystemProvider(NoOpFileSystemProvider.scheme, this, { 35 | isCaseSensitive: true, 36 | }); 37 | } 38 | } 39 | 40 | export const noOpFileSystemProvider = new NoOpFileSystemProvider(); 41 | -------------------------------------------------------------------------------- /src/quick-fix.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { LONG_RUN_TROUBLESHOOTING_URL, TROUBLESHOOTING_URL, extensionName } from './appGlobals'; 3 | import { WizardTaskId } from './setup-wizard'; 4 | 5 | export type QuickFixActionType = 6 | | 'help' 7 | | 'wizard' 8 | | 'disable-folder' 9 | | 'defer' 10 | | 'help-long-run' 11 | | 'setup-cmdline' 12 | | 'setup-monorepo'; 13 | 14 | interface QuickFixItem extends vscode.QuickPickItem { 15 | action: () => void; 16 | } 17 | 18 | /** 19 | * Showing configurable quick fix menu via a quick pick 20 | * 21 | * @param folderName 22 | * @param types 23 | */ 24 | export const showQuickFix = async (folderName: string, types: QuickFixActionType[]) => { 25 | const buildItems = (): QuickFixItem[] => { 26 | const setupToolItem = (taskId?: WizardTaskId): QuickFixItem => ({ 27 | label: '$(tools) Customize Extension', 28 | description: 'if you can run jest via CLI but not via the extension', 29 | action: () => { 30 | vscode.commands.executeCommand( 31 | `${extensionName}.with-workspace.setup-extension`, 32 | folderName, 33 | taskId && { taskId } 34 | ); 35 | }, 36 | }); 37 | 38 | const items: QuickFixItem[] = []; 39 | for (const t of types) { 40 | switch (t) { 41 | case 'help': 42 | items.push({ 43 | label: '$(info) Help', 44 | description: 'See troubleshooting guide', 45 | action: () => { 46 | vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(TROUBLESHOOTING_URL)); 47 | }, 48 | }); 49 | break; 50 | case 'wizard': 51 | items.push(setupToolItem()); 52 | break; 53 | case 'setup-cmdline': 54 | items.push(setupToolItem('cmdLine')); 55 | break; 56 | case 'setup-monorepo': 57 | items.push(setupToolItem('monorepo')); 58 | break; 59 | case 'disable-folder': 60 | items.push({ 61 | label: '$(error) Disable Extension', 62 | description: "if you don't intend to run jest in this folder ever", 63 | action: () => { 64 | vscode.commands.executeCommand(`${extensionName}.with-workspace.disable`, folderName); 65 | }, 66 | }); 67 | break; 68 | case 'defer': 69 | items.push({ 70 | label: '$(play) Defer or Change Run Mode', 71 | description: 'if you are not ready to run jest yet', 72 | action: () => { 73 | vscode.commands.executeCommand( 74 | `${extensionName}.with-workspace.change-run-mode`, 75 | folderName 76 | ); 77 | }, 78 | }); 79 | break; 80 | case 'help-long-run': 81 | items.push({ 82 | label: '$(info) Help', 83 | description: 'See LongRun troubleshooting guide', 84 | action: () => { 85 | vscode.commands.executeCommand( 86 | 'vscode.open', 87 | vscode.Uri.parse(LONG_RUN_TROUBLESHOOTING_URL) 88 | ); 89 | }, 90 | }); 91 | break; 92 | } 93 | } 94 | return items; 95 | }; 96 | 97 | const items = buildItems(); 98 | const item = await vscode.window.showQuickPick(items, { placeHolder: 'Select a fix action' }); 99 | item?.action(); 100 | }; 101 | -------------------------------------------------------------------------------- /src/reporter.ts: -------------------------------------------------------------------------------- 1 | // Custom Jest reporter used by jest-vscode extension 2 | 3 | import type { AggregatedResult, Test, TestResult } from '@jest/test-result'; 4 | import { Reporter, TestContext } from '@jest/reporters'; 5 | 6 | class VSCodeJestReporter implements Reporter { 7 | onRunStart(aggregatedResults: AggregatedResult): void { 8 | process.stderr.write( 9 | `onRunStart: numTotalTestSuites: ${aggregatedResults.numTotalTestSuites}\r\n` 10 | ); 11 | } 12 | 13 | onRunComplete(_contexts: Set, results: AggregatedResult): void { 14 | // report exec errors that could have prevented Result file being generated 15 | if (results.runExecError) { 16 | process.stderr.write(`onRunComplete: execError: ${results.runExecError.message}\r\n`); 17 | } else { 18 | process.stderr.write('onRunComplete\r\n'); 19 | } 20 | } 21 | 22 | // report any test or exec errors 23 | onTestFileResult( 24 | _test: Test, 25 | testResult: TestResult, 26 | _aggregatedResult: AggregatedResult 27 | ): Promise | void { 28 | if (testResult.numFailingTests > 0 || testResult.testExecError) { 29 | const msg = `onTestFileResult: encountered errors`; 30 | process.stderr.write(`${msg}\r\n`); 31 | } 32 | } 33 | 34 | getLastError(): Error | undefined { 35 | return; 36 | } 37 | } 38 | 39 | module.exports = VSCodeJestReporter; 40 | -------------------------------------------------------------------------------- /src/setup-wizard/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | startWizard, 3 | StartWizardOptions, 4 | WizardTaskId, 5 | PendingSetupTask, 6 | PendingSetupTaskKey, 7 | } from './start-wizard'; 8 | 9 | export { IgnoreWorkspaceChanges } from './tasks'; 10 | -------------------------------------------------------------------------------- /src/setup-wizard/start-wizard.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { 3 | WizardStatus, 4 | ActionableMenuItem, 5 | WizardContext, 6 | SetupTask, 7 | WIZARD_HELP_URL, 8 | } from './types'; 9 | import { jsonOut, actionItem, showActionMenu } from './wizard-helper'; 10 | import { setupJestCmdLine, setupJestDebug, setupMonorepo } from './tasks'; 11 | import { ExtOutputTerminal, OutputOptions } from '../JestExt/output-terminal'; 12 | import { toErrorString } from '../helpers'; 13 | import { WorkspaceManager } from '../workspace-manager'; 14 | import { DebugConfigurationProvider } from '../DebugConfigurationProvider'; 15 | 16 | // wizard tasks - right now only 2, could easily add more 17 | export type WizardTaskId = 'cmdLine' | 'debugConfig' | 'monorepo'; 18 | 19 | export const PendingSetupTaskKey = 'jest.PendingSetupTask'; 20 | export interface PendingSetupTask { 21 | workspace: string; 22 | taskId: WizardTaskId; 23 | } 24 | 25 | export const StartWizardActionId: Record = { 26 | cmdLine: 0, 27 | debugConfig: 1, 28 | monorepo: 2, 29 | exit: 3, 30 | }; 31 | 32 | export const WizardTasks: { [key in WizardTaskId]: { task: SetupTask; actionId: number } } = { 33 | ['cmdLine']: { task: setupJestCmdLine, actionId: StartWizardActionId.cmdLine }, 34 | ['debugConfig']: { task: setupJestDebug, actionId: StartWizardActionId.debugConfig }, 35 | ['monorepo']: { task: setupMonorepo, actionId: StartWizardActionId.monorepo }, 36 | }; 37 | 38 | export interface StartWizardOptions { 39 | workspace?: vscode.WorkspaceFolder; 40 | taskId?: WizardTaskId; 41 | verbose?: boolean; 42 | } 43 | export const startWizard = ( 44 | debugConfigProvider: DebugConfigurationProvider, 45 | vscodeContext: vscode.ExtensionContext, 46 | options: StartWizardOptions = {} 47 | ): Promise => { 48 | const { workspace, taskId, verbose } = options; 49 | 50 | const terminal = new ExtOutputTerminal('vscode-jest Setup Tool', true); 51 | 52 | const message = (msg: string, opt?: OutputOptions): string => { 53 | const str = terminal.write(`${msg}${opt ? '' : '\r\n'}`, opt); 54 | if (verbose) { 55 | console.log(` ${msg}`); 56 | } 57 | return str; 58 | }; 59 | 60 | const runTask = async (context: WizardContext, taskId: WizardTaskId): Promise => { 61 | try { 62 | const wsMsg = context.workspace ? `in workspace "${context.workspace.name}"` : ''; 63 | message(`=== starting ${taskId} task ${wsMsg} ===\r\n`, 'new-line'); 64 | 65 | const result = await WizardTasks[taskId].task(context); 66 | message(`=== ${taskId} task completed with status "${result}" ===\r\n`, 'new-line'); 67 | return result; 68 | } catch (e) { 69 | message(`setup ${taskId} task encountered unexpected error:\r\n${toErrorString(e)}`, 'error'); 70 | } 71 | return 'error'; 72 | }; 73 | 74 | const showMainMenu = async (context: WizardContext): Promise => { 75 | const menuItems: ActionableMenuItem[] = [ 76 | actionItem( 77 | StartWizardActionId.cmdLine, 78 | '$(beaker) Setup Jest Command', 79 | 'set up jest command to run your tests', 80 | () => runTask(context, 'cmdLine') 81 | ), 82 | actionItem( 83 | StartWizardActionId.debugConfig, 84 | '$(debug-alt) Setup Jest Debug Config', 85 | 'setup launch.json to debug jest tests', 86 | () => runTask(context, 'debugConfig') 87 | ), 88 | actionItem( 89 | StartWizardActionId.monorepo, 90 | '$(folder-library) Setup Monorepo Project', 91 | 'setup and validate workspaces for monorepo project', 92 | () => runTask(context, 'monorepo') 93 | ), 94 | actionItem(StartWizardActionId.exit, '$(close) Exit', 'Exit the setup tool', () => 95 | Promise.resolve('exit') 96 | ), 97 | ]; 98 | 99 | let result: WizardStatus; 100 | let selectItemIdx: number | undefined = menuItems.findIndex( 101 | (item) => taskId && item.id === WizardTasks[taskId]?.actionId 102 | ); 103 | do { 104 | result = await showActionMenu(menuItems, { 105 | title: 'vscode-jest Setup Tool', 106 | placeholder: 'select a set up action below', 107 | selectItemIdx, 108 | verbose, 109 | }); 110 | selectItemIdx = undefined; 111 | } while (result !== 'exit' && result !== 'error'); 112 | return result; 113 | }; 114 | 115 | const launch = async (): Promise => { 116 | message(`Setup Tool Guide: ${WIZARD_HELP_URL}`, 'info'); 117 | terminal.show(); 118 | 119 | const context: WizardContext = { 120 | debugConfigProvider, 121 | wsManager: new WorkspaceManager(), 122 | vscodeContext, 123 | workspace, 124 | message, 125 | verbose, 126 | }; 127 | 128 | try { 129 | const s = await showMainMenu(context); 130 | const status = s === 'exit' ? 'success' : s; 131 | message(`\nsetup-tool exit with status "${status}"`); 132 | return status; 133 | } catch (e) { 134 | console.error(`setup-tool caught error:`, e); 135 | message(`\nsetup-tool exit with error: ${jsonOut(e)}`); 136 | return 'error'; 137 | } 138 | }; 139 | 140 | vscodeContext.globalState.update(PendingSetupTaskKey, undefined); 141 | return launch(); 142 | }; 143 | -------------------------------------------------------------------------------- /src/setup-wizard/tasks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './setup-jest-cmdline'; 2 | export * from './setup-jest-debug'; 3 | export * from './setup-monorepo'; 4 | -------------------------------------------------------------------------------- /src/setup-wizard/types.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { DebugConfigurationProvider, DebugConfigOptions } from '../DebugConfigurationProvider'; 3 | import { JestExtOutput } from '../JestExt/output-terminal'; 4 | import { WorkspaceManager } from '../workspace-manager'; 5 | 6 | export interface WizardContext { 7 | debugConfigProvider: DebugConfigurationProvider; 8 | wsManager: WorkspaceManager; 9 | vscodeContext: vscode.ExtensionContext; 10 | workspace?: vscode.WorkspaceFolder; 11 | message: JestExtOutput['write']; 12 | verbose?: boolean; 13 | } 14 | 15 | export type WizardStatus = 'success' | 'error' | 'abort' | 'exit' | undefined; 16 | export type WizardAction = () => Promise; 17 | interface ActionableComp { 18 | id: number; 19 | action?: WizardAction; 20 | } 21 | export type ActionableMenuItem = vscode.QuickPickItem & ActionableComp; 22 | export type ActionableButton = vscode.QuickInputButton & ActionableComp; 23 | export type ActionableMessageItem = vscode.MessageItem & ActionableComp; 24 | 25 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 26 | export const isActionableButton = (arg: any): arg is ActionableButton => 27 | arg && arg.iconPath && typeof arg.action === 'function'; 28 | 29 | export type ActionMessageType = 'info' | 'warning' | 'error'; 30 | 31 | export type AllowBackButton = { enableBackButton?: boolean }; 32 | export type Verbose = { verbose?: boolean }; 33 | 34 | // actionable menu 35 | export type ActionMenuInput = ActionableMenuItem | ActionableButton | undefined; 36 | export type ActionableMenuResult = T | undefined; 37 | export interface ActionMenuOptions extends AllowBackButton, Verbose { 38 | title?: string; 39 | placeholder?: string; 40 | value?: string; 41 | rightButtons?: ActionableButton[]; 42 | selectItemIdx?: number; 43 | // if true, treat action item/button without action as no-op; otherwise exit with "undefined" 44 | allowNoAction?: boolean; 45 | } 46 | 47 | // actionable input box 48 | export type ActionInputResult = T | string | undefined; 49 | export type ActionInput = ActionInputResult | ActionableButton | undefined; 50 | export interface ActionInputBoxOptions extends AllowBackButton, Verbose { 51 | title?: string; 52 | prompt?: string; 53 | value?: string; 54 | rightButtons?: ActionableButton[]; 55 | } 56 | 57 | export type SetupTask = (context: WizardContext) => Promise; 58 | 59 | // settings 60 | export interface WizardSettings extends DebugConfigOptions { 61 | absoluteRootPath?: string; 62 | configurations?: vscode.DebugConfiguration[]; 63 | } 64 | 65 | export interface ConfigEntry { 66 | name: string; 67 | value: unknown; 68 | } 69 | 70 | export const WIZARD_HELP_URL = 71 | 'https://github.com/jest-community/vscode-jest/blob/master/setup-wizard.md'; 72 | -------------------------------------------------------------------------------- /src/terminal-link-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | type ExecutableTerminalLink = vscode.TerminalLink & { data: string }; 4 | 5 | export const ExecutableLinkScheme = 'vscode-jest'; 6 | 7 | /** 8 | * provide terminal links for commands that can be executed in the terminal. 9 | * 10 | * The link data is a vscode uri with the following format: 11 | * vscode-jest:///? 12 | * 13 | * Note the folderName, command, and args should be encoded using encodeURIComponent 14 | * The args should be a JSON.stringify-able object and the command should expect to receive them accordingly. 15 | * 16 | * example: 17 | * vscode-jest://workspace%20name/io.orta.jest.with-workspace.setup-extension 18 | */ 19 | export class ExecutableTerminalLinkProvider 20 | implements vscode.TerminalLinkProvider 21 | { 22 | async handleTerminalLink(link: ExecutableTerminalLink): Promise { 23 | try { 24 | const uri = vscode.Uri.parse(link.data); 25 | const folderName = decodeURIComponent(uri.authority); 26 | const command = decodeURIComponent(uri.path).substring(1); 27 | const args = uri.query && JSON.parse(decodeURIComponent(uri.query)); 28 | await vscode.commands.executeCommand(command, folderName, args); 29 | } catch (error) { 30 | vscode.window.showErrorMessage(`Failed to handle link "${link.data}": ${error}`); 31 | } 32 | } 33 | 34 | provideTerminalLinks( 35 | context: vscode.TerminalLinkContext, 36 | _token: vscode.CancellationToken 37 | ): vscode.ProviderResult { 38 | const uriRegex = new RegExp(`${ExecutableLinkScheme}://[^\\s]+`, 'g'); 39 | const links: ExecutableTerminalLink[] = []; 40 | for (const match of context.line.matchAll(uriRegex)) { 41 | if (match.index !== undefined) { 42 | links.push({ 43 | startIndex: match.index, 44 | length: match[0].length, 45 | tooltip: 'execute command', 46 | data: match[0], 47 | }); 48 | } else { 49 | // Handle the unexpected case where index is undefined 50 | console.error('Unexpected undefined match index'); 51 | } 52 | } 53 | 54 | return links.length > 0 ? links : []; 55 | } 56 | register(): vscode.Disposable { 57 | return vscode.window.registerTerminalLinkProvider(this); 58 | } 59 | 60 | /** 61 | * create a link that can be executed in the terminal 62 | * @param folderName 63 | * @param command 64 | * @param arg any JSON.stringify-able object 65 | * @returns 66 | */ 67 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 68 | public executableLink(folderName: string, command: string, arg?: any): string { 69 | const baseLink = `${ExecutableLinkScheme}://${encodeURIComponent( 70 | folderName 71 | )}/${encodeURIComponent(command)}`; 72 | if (!arg) { 73 | return baseLink; 74 | } 75 | const encodedQuery = encodeURIComponent(JSON.stringify(arg)); 76 | return `${baseLink}?${encodedQuery}`; 77 | } 78 | } 79 | 80 | export const executableTerminalLinkProvider = new ExecutableTerminalLinkProvider(); 81 | -------------------------------------------------------------------------------- /src/test-provider/index.ts: -------------------------------------------------------------------------------- 1 | export { JestTestProvider } from './test-provider'; 2 | -------------------------------------------------------------------------------- /src/test-provider/test-item-context-manager.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { extensionName } from '../appGlobals'; 3 | import { ItemCommand } from './types'; 4 | 5 | /** 6 | * A cross-workspace Context Manager that manages the testId context used in 7 | * TestExplorer menu when-condition 8 | */ 9 | 10 | export interface SnapshotItem { 11 | itemId: string; 12 | testFullName: string; 13 | } 14 | export type ItemContext = { 15 | key: 16 | | 'jest.runMode' 17 | | 'jest.editor-view-snapshot' 18 | | 'jest.editor-update-snapshot' 19 | | 'jest.workspaceRoot'; 20 | workspace: vscode.WorkspaceFolder; 21 | itemIds: string[]; 22 | }; 23 | export type TEItemContextKey = ItemContext['key']; 24 | 25 | export class TestItemContextManager { 26 | private cache = new Map(); 27 | private wsCache: Record = {}; 28 | 29 | // context are stored by key, one per workspace 30 | private updateContextCache(context: ItemContext): ItemContext[] { 31 | this.wsCache[context.workspace.name] = context.workspace; 32 | let list = this.cache.get(context.key); 33 | if (!list) { 34 | list = [context]; 35 | } else { 36 | list = list.filter((c) => c.workspace.name !== context.workspace.name).concat(context); 37 | } 38 | this.cache.set(context.key, list); 39 | return list; 40 | } 41 | public setItemContext(context: ItemContext): void { 42 | const list = this.updateContextCache(context); 43 | switch (context.key) { 44 | case 'jest.runMode': 45 | case 'jest.editor-view-snapshot': 46 | case 'jest.editor-update-snapshot': 47 | case 'jest.workspaceRoot': { 48 | const itemIds = list.flatMap((c) => c.itemIds); 49 | vscode.commands.executeCommand('setContext', context.key, itemIds); 50 | } 51 | } 52 | } 53 | private getItemWorkspace(item: vscode.TestItem): vscode.WorkspaceFolder | undefined { 54 | let target = item; 55 | while (target.parent) { 56 | target = target.parent; 57 | } 58 | const workspace = this.wsCache[target.id.split(':')[1]]; 59 | return workspace ?? (item.uri && vscode.workspace.getWorkspaceFolder(item.uri)); 60 | } 61 | 62 | public registerCommands(): vscode.Disposable[] { 63 | const revealOutputCommand = vscode.commands.registerCommand( 64 | `${extensionName}.test-item.reveal-output`, 65 | (testItem: vscode.TestItem) => { 66 | const workspace = this.getItemWorkspace(testItem); 67 | if (workspace) { 68 | vscode.commands.executeCommand( 69 | `${extensionName}.with-workspace.item-command`, 70 | workspace, 71 | testItem, 72 | ItemCommand.revealOutput 73 | ); 74 | } 75 | } 76 | ); 77 | const runModeCommand = vscode.commands.registerCommand( 78 | `${extensionName}.test-item.run-mode.change`, 79 | (testItem: vscode.TestItem) => { 80 | const workspace = this.getItemWorkspace(testItem); 81 | if (workspace) { 82 | vscode.commands.executeCommand( 83 | `${extensionName}.with-workspace.change-run-mode`, 84 | workspace 85 | ); 86 | } 87 | } 88 | ); 89 | 90 | const viewSnapshotCommand = vscode.commands.registerCommand( 91 | `${extensionName}.test-item.view-snapshot`, 92 | (testItem: vscode.TestItem) => { 93 | const workspace = this.getItemWorkspace(testItem); 94 | if (workspace) { 95 | vscode.commands.executeCommand( 96 | `${extensionName}.with-workspace.item-command`, 97 | workspace, 98 | testItem, 99 | ItemCommand.viewSnapshot 100 | ); 101 | } 102 | } 103 | ); 104 | const updateSnapshotCommand = vscode.commands.registerCommand( 105 | `${extensionName}.test-item.update-snapshot`, 106 | (testItem: vscode.TestItem) => { 107 | const workspace = this.getItemWorkspace(testItem); 108 | if (workspace) { 109 | vscode.commands.executeCommand( 110 | `${extensionName}.with-workspace.item-command`, 111 | workspace, 112 | testItem, 113 | ItemCommand.updateSnapshot 114 | ); 115 | } 116 | } 117 | ); 118 | 119 | return [runModeCommand, viewSnapshotCommand, updateSnapshotCommand, revealOutputCommand]; 120 | } 121 | } 122 | 123 | export const tiContextManager = new TestItemContextManager(); 124 | -------------------------------------------------------------------------------- /src/test-provider/test-provider-context.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { JestOutputTerminal } from '../JestExt/output-terminal'; 3 | import { JestExtExplorerContext, TestItemData } from './types'; 4 | import { JestTestRun } from './jest-test-run'; 5 | 6 | /** 7 | * provide context information from JestExt and test provider state: 8 | * 1. TestData <-> TestItem 9 | * 10 | * as well as factory functions to create TestItem and TestRun that could impact the state 11 | */ 12 | 13 | export type TagIdType = 'run' | 'debug' | 'update-snapshot'; 14 | 15 | export interface JestTestRunOptions { 16 | name?: string; 17 | } 18 | export type CreateTestRun = vscode.TestController['createTestRun']; 19 | 20 | let SEQ = 0; 21 | export class JestTestProviderContext { 22 | private testItemData: WeakMap; 23 | 24 | constructor( 25 | public readonly ext: JestExtExplorerContext, 26 | private readonly controller: vscode.TestController, 27 | private readonly profiles: vscode.TestRunProfile[] 28 | ) { 29 | this.testItemData = new WeakMap(); 30 | } 31 | get output(): JestOutputTerminal { 32 | return this.ext.output; 33 | } 34 | 35 | createTestItem = ( 36 | id: string, 37 | label: string, 38 | uri: vscode.Uri, 39 | data: TestItemData, 40 | parent?: vscode.TestItem, 41 | tagIds: TagIdType[] = ['run', 'debug'] 42 | ): vscode.TestItem => { 43 | const testItem = this.controller.createTestItem(id, label, uri); 44 | this.testItemData.set(testItem, data); 45 | const collection = parent ? parent.children : this.controller.items; 46 | collection.add(testItem); 47 | 48 | tagIds?.forEach((tId) => { 49 | const tag = this.getTag(tId); 50 | if (tag) { 51 | testItem.tags = [...testItem.tags, tag]; 52 | } 53 | }); 54 | 55 | return testItem; 56 | }; 57 | 58 | /** 59 | * check if there is such child in the item, if exists returns the associated data 60 | * 61 | * @param item 62 | * @param childId id of the child item 63 | * @returns data of the child item, casting for easy usage but does not guarantee type safety. 64 | */ 65 | getChildData = ( 66 | item: vscode.TestItem, 67 | childId: string 68 | ): T | undefined => { 69 | const cItem = item.children.get(childId); 70 | 71 | // Note: casting for easy usage but does not guarantee type safety. 72 | return cItem && (this.testItemData.get(cItem) as T); 73 | }; 74 | 75 | /** 76 | * get data associated with the item. All item used here should have some data associated with, otherwise 77 | * an exception will be thrown 78 | * 79 | * @returns casting for easy usage but does not guarantee type safety 80 | */ 81 | getData = (item: vscode.TestItem): T | undefined => { 82 | // Note: casting for easy usage but does not guarantee type safety. 83 | return this.testItemData.get(item) as T | undefined; 84 | }; 85 | 86 | createTestRun = (request: vscode.TestRunRequest, options?: JestTestRunOptions): JestTestRun => { 87 | const name = options?.name ?? `testRun-${SEQ++}`; 88 | return new JestTestRun(name, this, request, this.controller.createTestRun); 89 | }; 90 | 91 | // tags 92 | getTag = (tagId: TagIdType): vscode.TestTag => { 93 | const tag = this.profiles.find((p) => p.tag?.id === tagId)?.tag; 94 | if (!tag) { 95 | throw new Error(`unrecognized tag: ${tagId}`); 96 | } 97 | return tag; 98 | }; 99 | 100 | /** 101 | * Create a new request based on the given one, which could be based on outdated data. 102 | * This is mainly used to support deferred mode: when the request is created during deferred mode on, it will need to be updated with new test items after existing deferred mode because the test tree has been rebuilt. 103 | * @param request 104 | * @returns 105 | */ 106 | requestFrom = (request: vscode.TestRunRequest): vscode.TestRunRequest => { 107 | const findItem = (item: vscode.TestItem, collection: vscode.TestItemCollection) => { 108 | let found = collection.get(item.id); 109 | if (!found) { 110 | collection.forEach((cItem) => { 111 | if (!found && cItem.children) { 112 | found = findItem(item, cItem.children); 113 | } 114 | }); 115 | } 116 | return found; 117 | }; 118 | const mapItems = (items?: readonly vscode.TestItem[]) => 119 | items && 120 | items.map((i) => { 121 | const found = findItem(i, this.controller.items); 122 | if (found) { 123 | return found; 124 | } 125 | throw new Error(`failed to find item ${i.id}`); 126 | }); 127 | 128 | const include = mapItems(request.include); 129 | const exclude = mapItems(request.exclude); 130 | const profile = 131 | request.profile && this.profiles.find((p) => p.label === request.profile?.label); 132 | if (request.profile && !profile) { 133 | throw new Error(`failed to find profile ${request.profile.label}`); 134 | } 135 | 136 | return new vscode.TestRunRequest(include, exclude, profile); 137 | }; 138 | } 139 | -------------------------------------------------------------------------------- /src/test-provider/types.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { DebugFunction, JestSessionEvents, JestExtSessionContext } from '../JestExt'; 3 | import { TestResultProvider } from '../TestResults'; 4 | import { WorkspaceRoot, FolderData, TestData, TestDocumentRoot } from './test-item-data'; 5 | import { JestTestProviderContext } from './test-provider-context'; 6 | import { JestTestRun } from './jest-test-run'; 7 | import { DebugInfo } from '../types'; 8 | 9 | export type TestItemDataType = WorkspaceRoot | FolderData | TestDocumentRoot | TestData; 10 | 11 | /** JestExt context exposed to the test explorer */ 12 | export interface JestExtExplorerContext extends JestExtSessionContext { 13 | readonly testResultProvider: TestResultProvider; 14 | readonly sessionEvents: JestSessionEvents; 15 | debugTests: DebugFunction; 16 | } 17 | 18 | export interface ScheduleTestOptions { 19 | itemCommand?: ItemCommand; 20 | profile?: vscode.TestRunProfile; 21 | } 22 | 23 | export interface TestItemData { 24 | readonly item: vscode.TestItem; 25 | readonly uri: vscode.Uri; 26 | context: JestTestProviderContext; 27 | discoverTest?: (run: JestTestRun) => void; 28 | scheduleTest: (run: JestTestRun, options?: ScheduleTestOptions) => void; 29 | runItemCommand: (command: ItemCommand) => void; 30 | getDebugInfo: () => DebugInfo; 31 | } 32 | 33 | export enum TestTagId { 34 | Run = 'run', 35 | Debug = 'debug', 36 | } 37 | 38 | export enum ItemCommand { 39 | updateSnapshot = 'update-snapshot', 40 | viewSnapshot = 'view-snapshot', 41 | revealOutput = 'reveal-output', 42 | } 43 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export interface DecorationOptions extends vscode.DecorationOptions { 4 | identifier: string; 5 | } 6 | 7 | export interface TestStats { 8 | success: number; 9 | fail: number; 10 | unknown: number; 11 | } 12 | export type TestStatsCategory = keyof TestStats; 13 | 14 | export interface TestExplorerRunRequest { 15 | request: vscode.TestRunRequest; 16 | token: vscode.CancellationToken; 17 | } 18 | 19 | export interface StringPattern { 20 | value: string; 21 | exactMatch?: boolean; 22 | isRegExp?: boolean; 23 | } 24 | 25 | export type TestNamePattern = StringPattern | string; 26 | export interface DebugInfo { 27 | testPath: string; 28 | useTestPathPattern?: boolean; 29 | testName?: TestNamePattern; 30 | } 31 | -------------------------------------------------------------------------------- /src/virtual-workspace-folder.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { toAbsoluteRootPath } from './helpers'; 3 | 4 | export interface FolderAwareItem { 5 | workspaceFolder: vscode.WorkspaceFolder; 6 | } 7 | 8 | export class VirtualFolderBasedCache { 9 | // cache folder by its name, which could be either the actual or virtual workspace folder name 10 | private byFolderName: Record; 11 | // group folder list by its actual folder name 12 | private byActualFolderName: Record; 13 | 14 | constructor() { 15 | this.byFolderName = {}; 16 | this.byActualFolderName = {}; 17 | } 18 | 19 | get size(): number { 20 | return Object.keys(this.byFolderName).length; 21 | } 22 | 23 | /** get all cached items */ 24 | getAllItems(): T[] { 25 | return Object.values(this.byFolderName); 26 | } 27 | /** 28 | * Adds an item to the cache. If an item with the same workspace folder name already exists, it will be replaced. 29 | * The item will also be added to the list of items under the actual folder name. 30 | * @param item The item to add to the cache. 31 | */ 32 | addItem(item: T) { 33 | this.byFolderName[item.workspaceFolder.name] = item; 34 | const actualFolderName = isVirtualWorkspaceFolder(item.workspaceFolder) 35 | ? item.workspaceFolder.actualWorkspaceFolder.name 36 | : item.workspaceFolder.name; 37 | let items = this.byActualFolderName[actualFolderName] ?? []; 38 | 39 | // in case the item is already in the list, remove it first 40 | items = items.filter((i) => i.workspaceFolder.name !== item.workspaceFolder.name); 41 | 42 | items.push(item); 43 | this.byActualFolderName[actualFolderName] = items; 44 | } 45 | deleteItemByFolder(workspaceFolder: vscode.WorkspaceFolder) { 46 | delete this.byFolderName[workspaceFolder.name]; 47 | 48 | if (isVirtualWorkspaceFolder(workspaceFolder)) { 49 | // delete the virtual folder from the actual folder 50 | let items = this.byActualFolderName[workspaceFolder.actualWorkspaceFolder.name]; 51 | items = items?.filter((i) => i.workspaceFolder.name !== workspaceFolder.name); 52 | this.byActualFolderName[workspaceFolder.actualWorkspaceFolder.name] = items; 53 | } else { 54 | // delete all the virtual folders under the actual folder 55 | const items = this.byActualFolderName[workspaceFolder.name]; 56 | items?.forEach((item) => delete this.byFolderName[item.workspaceFolder.name]); 57 | delete this.byActualFolderName[workspaceFolder.name]; 58 | } 59 | } 60 | getItemByFolderName(name: string): T | undefined { 61 | return this.byFolderName[name]; 62 | } 63 | getItemsByActualFolderName(actualFolderName: string): T[] | undefined { 64 | return this.byActualFolderName[actualFolderName]; 65 | } 66 | findRelatedItems(uri: vscode.Uri): T[] | undefined { 67 | const checkVirtualFolder = (includeActualFolder: boolean) => (item: T) => 68 | isVirtualWorkspaceFolder(item.workspaceFolder) 69 | ? item.workspaceFolder.isInWorkspaceFolder(uri) 70 | : includeActualFolder; 71 | 72 | const actualFolder = vscode.workspace.getWorkspaceFolder(uri); 73 | if (actualFolder) { 74 | const items = this.getItemsByActualFolderName(actualFolder.name); 75 | return items?.filter(checkVirtualFolder(true)); 76 | } 77 | // if the file is not in any actual workspace folder, try all virtual folders 78 | return this.getAllItems().filter(checkVirtualFolder(false)); 79 | } 80 | 81 | reset() { 82 | this.byFolderName = {}; 83 | this.byActualFolderName = {}; 84 | } 85 | } 86 | 87 | /** 88 | * A virtual workspace folder is a folder resides in a physical workspace folder but might have 89 | * different name and separate jest settings. A physical workspace folder can have multiple virtual folders. 90 | 91 | * Note: The class will have the same index and the uri as the actual workspace folder, but a different name. 92 | */ 93 | export class VirtualWorkspaceFolder implements vscode.WorkspaceFolder { 94 | /** URI pointing to the virtual folder, including rootPath */ 95 | public readonly effectiveUri: vscode.Uri; 96 | 97 | constructor( 98 | public readonly actualWorkspaceFolder: vscode.WorkspaceFolder, 99 | public readonly name: string, 100 | rootPath?: string 101 | ) { 102 | this.effectiveUri = rootPath 103 | ? vscode.Uri.file(toAbsoluteRootPath(actualWorkspaceFolder, rootPath)) 104 | : actualWorkspaceFolder.uri; 105 | } 106 | 107 | get index(): number { 108 | return this.actualWorkspaceFolder.index; 109 | } 110 | get uri(): vscode.Uri { 111 | return this.actualWorkspaceFolder.uri; 112 | } 113 | 114 | /** Check if the given uri falls within the virtual folder's path */ 115 | isInWorkspaceFolder(uri: vscode.Uri): boolean { 116 | return uri.fsPath.startsWith(this.effectiveUri.fsPath); 117 | } 118 | } 119 | 120 | export const isVirtualWorkspaceFolder = ( 121 | workspaceFolder: vscode.WorkspaceFolder 122 | ): workspaceFolder is VirtualWorkspaceFolder => { 123 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 124 | return (workspaceFolder as any).actualWorkspaceFolder != undefined; 125 | }; 126 | -------------------------------------------------------------------------------- /syntaxes/LICENSE: -------------------------------------------------------------------------------- 1 | jest-snapshot.tmLanguage is forked from Microsoft/TypeScript-TmLanguage 2 | 3 | https://github.com/Microsoft/TypeScript-TmLanguage/blob/55e9f737b722895943e6647a9392bf286ed6af55/TypeScriptReact.tmLanguage 4 | 5 | Under this license: 6 | 7 | https://github.com/Microsoft/TypeScript-TmLanguage/blob/master/LICENSE.txt 8 | 9 | TypeScript Grammar 10 | 11 | Copyright (c) Microsoft Corporation 12 | All rights reserved. 13 | 14 | MIT License 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining a copy 17 | of this software and associated documentation files (the "Software"), to deal 18 | in the Software without restriction, including without limitation the rights 19 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | copies of the Software, and to permit persons to whom the Software is 21 | furnished to do so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in 24 | all copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 32 | THE SOFTWARE. 33 | -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // eslint override for tests only 2 | module.exports = { 3 | rules: { 4 | '@typescript-eslint/no-empty-function': 'off', 5 | '@typescript-eslint/no-explicit-any': 'off', 6 | '@typescript-eslint/explicit-function-return-type': 'off', 7 | 'jest/no-conditional-expect': 'off', 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /tests/Coverage/CoverageCodeLensProvider.test.ts: -------------------------------------------------------------------------------- 1 | jest.unmock('../../src/Coverage/CoverageCodeLensProvider'); 2 | 3 | const rangeConstructor = jest.fn(); 4 | jest.mock('vscode', () => { 5 | class CodeLens { 6 | range: any; 7 | command: any; 8 | 9 | constructor(range, command) { 10 | this.range = range; 11 | this.command = command; 12 | } 13 | } 14 | 15 | const EventEmitter = jest.fn(); 16 | 17 | class Position { 18 | lineNumber: string; 19 | character: string; 20 | 21 | constructor(lineNumber, character) { 22 | this.lineNumber = lineNumber; 23 | this.character = character; 24 | } 25 | } 26 | 27 | class Range { 28 | start: Position; 29 | end: Position; 30 | 31 | constructor(start, end) { 32 | rangeConstructor(); 33 | this.start = start; 34 | this.end = end; 35 | } 36 | } 37 | 38 | return { 39 | CodeLens, 40 | Position, 41 | Range, 42 | EventEmitter, 43 | }; 44 | }); 45 | import * as vscode from 'vscode'; 46 | import { CoverageCodeLensProvider } from '../../src/Coverage/CoverageCodeLensProvider'; 47 | 48 | describe('CoverageCodeLensProvider', () => { 49 | let mockJestExt; 50 | let provider; 51 | 52 | beforeEach(() => { 53 | mockJestExt = { 54 | coverageMapProvider: { getFileCoverage: jest.fn() }, 55 | coverageOverlay: { enabled: true }, 56 | name: 'venv1', 57 | }; 58 | const mockGetExt = jest.fn().mockReturnValue([mockJestExt]); 59 | provider = new CoverageCodeLensProvider(mockGetExt); 60 | }); 61 | describe('provideCodeLenses', () => { 62 | const doc = { fileName: 'file.js' }; 63 | const coverage = { 64 | toSummary: () => ({ 65 | toJSON: () => ({ 66 | branches: { pct: 10 }, 67 | lines: { pct: 46.15 }, 68 | }), 69 | }), 70 | }; 71 | 72 | test('do nothing when no coverage', () => { 73 | mockJestExt.coverageMapProvider.getFileCoverage = () => null; 74 | const result = provider.provideCodeLenses(doc); 75 | expect(result).toBeUndefined(); 76 | }); 77 | 78 | test('can summarize', () => { 79 | mockJestExt.coverageMapProvider.getFileCoverage = () => coverage; 80 | const result = provider.provideCodeLenses(doc); 81 | expect(result).toHaveLength(1); 82 | expect(result[0].command.title).toContain('branches: 10%, lines: 46.15%'); 83 | }); 84 | test('do nothing when coverage is disabled', () => { 85 | mockJestExt.coverageMapProvider.getFileCoverage = () => coverage; 86 | mockJestExt.coverageOverlay.enabled = false; 87 | const result = provider.provideCodeLenses(doc); 88 | expect(result).toBeUndefined(); 89 | }); 90 | test('provides trigger to update codeLens on demand', () => { 91 | const fireMock = jest.fn(); 92 | const event: any = { whatever: true }; 93 | (vscode.EventEmitter as jest.Mocked).mockImplementation(() => ({ 94 | event, 95 | fire: fireMock, 96 | })); 97 | provider = new CoverageCodeLensProvider(mockJestExt); 98 | expect(provider.onDidChangeCodeLenses).toEqual(event); 99 | 100 | provider.coverageChanged(); 101 | expect(fireMock).toHaveBeenCalled(); 102 | }); 103 | describe('venv', () => { 104 | test('can provide separate summaries for each qualified venv', () => { 105 | const mockJestExt2: any = { 106 | coverageMapProvider: { getFileCoverage: jest.fn() }, 107 | coverageOverlay: { enabled: true }, 108 | name: 'venv2', 109 | }; 110 | const coverage2 = { 111 | toSummary: () => ({ 112 | toJSON: () => ({ 113 | branches: { pct: 90 }, 114 | lines: { pct: 97.85 }, 115 | }), 116 | }), 117 | }; 118 | const mockGetExt = jest.fn().mockReturnValue([mockJestExt, mockJestExt2]); 119 | provider = new CoverageCodeLensProvider(mockGetExt); 120 | mockJestExt.coverageMapProvider.getFileCoverage = () => coverage; 121 | mockJestExt2.coverageMapProvider.getFileCoverage = () => coverage2; 122 | const result = provider.provideCodeLenses(doc); 123 | expect(result).toHaveLength(2); 124 | expect(result[0].command.title).toContain('branches: 10%, lines: 46.15%'); 125 | expect(result[0].command.title).toContain('venv1'); 126 | expect(result[1].command.title).toContain('branches: 90%, lines: 97.85%'); 127 | expect(result[1].command.title).toContain('venv2'); 128 | }); 129 | }); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /tests/Coverage/CoverageOverlay.test.ts: -------------------------------------------------------------------------------- 1 | jest.unmock('../../src/Coverage/CoverageOverlay'); 2 | 3 | const vscodeProperties = { 4 | window: { 5 | visibleTextEditors: jest.fn(), 6 | }, 7 | }; 8 | jest.mock('vscode', () => { 9 | const vscode = { 10 | OverviewRulerLane: {}, 11 | window: { 12 | createTextEditorDecorationType: jest.fn(), 13 | }, 14 | }; 15 | 16 | Object.defineProperty(vscode.window, 'visibleTextEditors', { 17 | get: () => vscodeProperties.window.visibleTextEditors(), 18 | }); 19 | 20 | return vscode; 21 | }); 22 | 23 | import { CoverageOverlay } from '../../src/Coverage/CoverageOverlay'; 24 | import { DefaultFormatter } from '../../src/Coverage/Formatters/DefaultFormatter'; 25 | import { GutterFormatter } from '../../src/Coverage/Formatters/GutterFormatter'; 26 | 27 | describe('CoverageOverlay', () => { 28 | const coverageMapProvider: any = {}; 29 | 30 | describe('constructor', () => { 31 | it('should set the default visibility', () => { 32 | const sut = new CoverageOverlay(null, coverageMapProvider); 33 | 34 | expect(sut.enabled).toBe(CoverageOverlay.defaultVisibility); 35 | }); 36 | 37 | it('should set the visibility if provided', () => { 38 | const enabled = !CoverageOverlay.defaultVisibility; 39 | const sut = new CoverageOverlay(null, coverageMapProvider, enabled); 40 | 41 | expect(sut.enabled).toBe(enabled); 42 | }); 43 | 44 | it('should set the default overlay formatter', () => { 45 | const sut = new CoverageOverlay(null, coverageMapProvider); 46 | 47 | expect(DefaultFormatter).toHaveBeenCalledWith(coverageMapProvider, undefined); 48 | expect(sut.formatter).toBeInstanceOf(DefaultFormatter); 49 | }); 50 | it('can be customized', () => { 51 | const colors = { covered: 'red' }; 52 | const sut = new CoverageOverlay(null, coverageMapProvider, false, 'GutterFormatter', colors); 53 | 54 | expect(sut.enabled).toBe(false); 55 | expect(GutterFormatter).toHaveBeenCalledWith(null, coverageMapProvider, colors); 56 | expect(sut.formatter).toBeInstanceOf(GutterFormatter); 57 | }); 58 | }); 59 | 60 | describe('enabled', () => { 61 | describe('get', () => { 62 | it('should return the overlay visibility', () => { 63 | const expected = true; 64 | const sut = new CoverageOverlay(null, coverageMapProvider, expected); 65 | 66 | expect(sut.enabled).toBe(expected); 67 | }); 68 | }); 69 | }); 70 | 71 | describe('updateVisibleEditors()', () => { 72 | it('should update each editor', () => { 73 | const editors = [{}, {}, {}]; 74 | vscodeProperties.window.visibleTextEditors.mockReturnValueOnce(editors); 75 | 76 | const sut = new CoverageOverlay(null, coverageMapProvider); 77 | sut.update = jest.fn(); 78 | sut.updateVisibleEditors(); 79 | 80 | for (let i = 0; i < editors.length; i += 1) { 81 | expect((sut.update as jest.Mock).mock.calls[i]).toEqual([editors[i]]); 82 | } 83 | }); 84 | }); 85 | 86 | describe('update()', () => { 87 | it('should do nothing if the editor does not have a valid document', () => { 88 | const sut = new CoverageOverlay(null, coverageMapProvider); 89 | 90 | const editor: any = {}; 91 | sut.update(editor); 92 | 93 | expect(sut.formatter.format).not.toHaveBeenCalled(); 94 | expect(sut.formatter.clear).not.toHaveBeenCalled(); 95 | }); 96 | 97 | it('should add the overlay when enabled', () => { 98 | const enabled = true; 99 | const sut = new CoverageOverlay(null, coverageMapProvider, enabled); 100 | 101 | const editor: any = { document: {} }; 102 | sut.update(editor); 103 | 104 | expect(sut.formatter.format).toHaveBeenCalledWith(editor); 105 | }); 106 | 107 | it('should remove the overlay when disabled', () => { 108 | const enabled = false; 109 | const sut = new CoverageOverlay(null, coverageMapProvider, enabled); 110 | 111 | const editor: any = { document: {} }; 112 | sut.update(editor); 113 | 114 | expect(sut.formatter.clear).toHaveBeenCalledWith(editor); 115 | }); 116 | }); 117 | it('supports formatter dispose', () => { 118 | const sut = new CoverageOverlay(null, coverageMapProvider); 119 | sut.dispose(); 120 | expect(sut.formatter.dispose).toHaveBeenCalledTimes(1); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /tests/Coverage/Formatters/DefaultFormatter.test.ts: -------------------------------------------------------------------------------- 1 | jest.unmock('../../../src/Coverage/Formatters/DefaultFormatter'); 2 | 3 | jest.mock('vscode', () => { 4 | return { 5 | OverviewRulerLane: {}, 6 | Range: jest.fn(), 7 | window: { 8 | createTextEditorDecorationType: jest 9 | .fn() 10 | .mockImplementation((options: vscode.DecorationRenderOptions) => ({ 11 | options, 12 | dispose: jest.fn(), 13 | })), 14 | }, 15 | }; 16 | }); 17 | 18 | import { DefaultFormatter } from '../../../src/Coverage/Formatters/DefaultFormatter'; 19 | import * as vscode from 'vscode'; 20 | 21 | const makeRange = (line: number) => ({ 22 | start: { line, character: 0 }, 23 | end: { line, character: 0 }, 24 | }); 25 | 26 | describe('DefaultFormatter', () => { 27 | const mockLineCoverageRanges = jest.fn(); 28 | const mockGetColorString = jest.fn(); 29 | const mockSetDecorations = jest.fn(); 30 | const coverageMapProvider: any = { 31 | getFileCoverage: () => ({}), 32 | }; 33 | const editor: any = { 34 | document: { 35 | fileName: {}, 36 | }, 37 | setDecorations: mockSetDecorations, 38 | }; 39 | 40 | let sut: DefaultFormatter; 41 | beforeEach(() => { 42 | jest.clearAllMocks(); 43 | 44 | sut = new DefaultFormatter(coverageMapProvider); 45 | sut.lineCoverageRanges = mockLineCoverageRanges; 46 | }); 47 | 48 | it('will decorate inline code and overviewRuler', () => { 49 | mockGetColorString.mockReturnValue('some-color'); 50 | DefaultFormatter.prototype.getColorString = mockGetColorString; 51 | 52 | sut = new DefaultFormatter(coverageMapProvider); 53 | [(sut.uncoveredLine as any).options, (sut.partiallyCoveredLine as any).options].forEach( 54 | (options) => { 55 | expect(options.isWholeLine).toBeTruthy(); 56 | expect(options.OverviewRulerLane).toEqual(vscode.OverviewRulerLane.Left); 57 | expect(options.backgroundColor).toEqual('some-color'); 58 | expect(options.overviewRulerColor).toEqual('some-color'); 59 | } 60 | ); 61 | }); 62 | 63 | describe('when no coverage', () => { 64 | beforeEach(() => { 65 | mockLineCoverageRanges.mockReturnValue({}); 66 | }); 67 | 68 | it('should clear all decorations', () => { 69 | sut.format(editor); 70 | expect(mockSetDecorations).toHaveBeenCalledTimes(2); 71 | expect(mockSetDecorations).toHaveBeenCalledWith(sut.uncoveredLine, []); 72 | expect(mockSetDecorations).toHaveBeenCalledWith(sut.partiallyCoveredLine, []); 73 | }); 74 | }); 75 | describe('with coverage', () => { 76 | const [range1, range2, range3] = [makeRange(1), makeRange(2), makeRange(3)]; 77 | beforeEach(() => { 78 | mockLineCoverageRanges.mockReturnValue({ 79 | covered: [range1], 80 | uncovered: [range2], 81 | 'partially-covered': [range3], 82 | }); 83 | }); 84 | it('should decorate uncovered and partially-covered ranges', () => { 85 | sut.format(editor); 86 | expect(mockSetDecorations).toHaveBeenCalledTimes(2); 87 | expect(mockSetDecorations).toHaveBeenCalledWith(sut.uncoveredLine, [range2]); 88 | expect(mockSetDecorations).toHaveBeenCalledWith(sut.partiallyCoveredLine, [range3]); 89 | }); 90 | it('can can clear decorator for the given editor', () => { 91 | sut.clear(editor); 92 | expect(mockSetDecorations).toHaveBeenCalledTimes(2); 93 | expect(mockSetDecorations).toHaveBeenCalledWith(sut.uncoveredLine, []); 94 | expect(mockSetDecorations).toHaveBeenCalledWith(sut.partiallyCoveredLine, []); 95 | }); 96 | }); 97 | 98 | it('can dispose decorator for all editors', () => { 99 | sut.dispose(); 100 | expect(sut.uncoveredLine.dispose).toHaveBeenCalledTimes(1); 101 | expect(sut.partiallyCoveredLine.dispose).toHaveBeenCalledTimes(1); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /tests/Coverage/Formatters/GutterFormatter.test.ts: -------------------------------------------------------------------------------- 1 | jest.unmock('../../../src/Coverage/Formatters/GutterFormatter'); 2 | 3 | jest.mock('vscode', () => { 4 | return { 5 | OverviewRulerLane: {}, 6 | Range: jest.fn(), 7 | Uri: { 8 | file: jest.fn().mockImplementation((f: string) => ({ 9 | with: (query: object) => ({ file: f, ...query }), 10 | })), 11 | }, 12 | window: { 13 | createTextEditorDecorationType: jest 14 | .fn() 15 | .mockImplementation((options: vscode.DecorationRenderOptions) => ({ 16 | options, 17 | dispose: jest.fn(), 18 | })), 19 | }, 20 | }; 21 | }); 22 | 23 | import { GutterFormatter } from '../../../src/Coverage/Formatters/GutterFormatter'; 24 | import * as vscode from 'vscode'; 25 | 26 | const makeRange = (line: number) => ({ 27 | start: { line, character: 0 }, 28 | end: { line, character: 0 }, 29 | }); 30 | 31 | jest.mock('../../../src/helpers', () => ({ 32 | prepareIconFile: (icon) => icon, 33 | })); 34 | 35 | describe('GutterFormatter', () => { 36 | const mockLineCoverageRanges = jest.fn(); 37 | const mockGetColorString = jest.fn(); 38 | const mockSetDecorations = jest.fn(); 39 | const coverageMapProvider: any = jest.fn(); 40 | const context: any = { 41 | asAbsolutePath: (path: string) => path, 42 | }; 43 | const editor: any = { 44 | document: { 45 | fileName: 'whatever', 46 | }, 47 | setDecorations: mockSetDecorations, 48 | }; 49 | 50 | let sut: GutterFormatter; 51 | beforeEach(() => { 52 | jest.clearAllMocks(); 53 | }); 54 | 55 | describe('decorators', () => { 56 | beforeEach(() => { 57 | mockGetColorString.mockReturnValue('some-color'); 58 | GutterFormatter.prototype.getColorString = mockGetColorString; 59 | 60 | sut = new GutterFormatter(context, coverageMapProvider); 61 | }); 62 | it('will decorate gutter with an Uri differ by color', () => { 63 | expect(vscode.Uri.file).toHaveBeenCalledTimes(3); 64 | 65 | const decorations = [sut.uncoveredLine, sut.partiallyCoveredLine, sut.coveredLine]; 66 | decorations.forEach((d) => { 67 | const options = (d as any).options; 68 | expect(options.isWholeLine).toBeFalsy(); 69 | expect(options.backgroundColor).toBeUndefined(); 70 | expect(options.gutterIconPath).toEqual( 71 | expect.objectContaining({ query: 'color=some-color' }) 72 | ); 73 | }); 74 | }); 75 | it('uncovered and partially-covered will mark overviewRuler', () => { 76 | const decorations = [sut.uncoveredLine, sut.partiallyCoveredLine]; 77 | decorations.forEach((d) => { 78 | const options = (d as any).options; 79 | expect(options.OverviewRulerLane).toEqual(vscode.OverviewRulerLane.Left); 80 | expect(options.overviewRulerColor).toEqual('some-color'); 81 | }); 82 | }); 83 | it('covered decorator does not mark overviewRuler', () => { 84 | const options = (sut.coveredLine as any).options; 85 | expect(options.OverviewRulerLane).toBeUndefined(); 86 | expect(options.overviewRulerColor).toBeUndefined(); 87 | }); 88 | }); 89 | describe('format', () => { 90 | beforeEach(() => { 91 | sut = new GutterFormatter(context, coverageMapProvider); 92 | sut.lineCoverageRanges = mockLineCoverageRanges; 93 | }); 94 | 95 | describe('when no coverage', () => { 96 | beforeEach(() => { 97 | mockLineCoverageRanges.mockReturnValue({}); 98 | }); 99 | 100 | it('should clear all decorations', () => { 101 | sut.format(editor); 102 | expect(mockSetDecorations).toHaveBeenCalledTimes(3); 103 | expect(mockSetDecorations).toHaveBeenCalledWith(sut.uncoveredLine, []); 104 | expect(mockSetDecorations).toHaveBeenCalledWith(sut.partiallyCoveredLine, []); 105 | expect(mockSetDecorations).toHaveBeenCalledWith(sut.coveredLine, []); 106 | }); 107 | }); 108 | describe('with coverage', () => { 109 | const [range1, range2, range3] = [makeRange(1), makeRange(2), makeRange(3)]; 110 | beforeEach(() => { 111 | mockLineCoverageRanges.mockReturnValue({ 112 | covered: [range1], 113 | uncovered: [range2], 114 | 'partially-covered': [range3], 115 | }); 116 | }); 117 | it('should decorate uncovered and partially-covered ranges', () => { 118 | sut.format(editor); 119 | expect(mockSetDecorations).toHaveBeenCalledTimes(3); 120 | expect(mockSetDecorations).toHaveBeenCalledWith(sut.uncoveredLine, [range2]); 121 | expect(mockSetDecorations).toHaveBeenCalledWith(sut.partiallyCoveredLine, [range3]); 122 | expect(mockSetDecorations).toHaveBeenCalledWith(sut.coveredLine, [range1]); 123 | }); 124 | it('can can clear decorator for the given editor', () => { 125 | sut.clear(editor); 126 | expect(mockSetDecorations).toHaveBeenCalledTimes(3); 127 | expect(mockSetDecorations).toHaveBeenCalledWith(sut.uncoveredLine, []); 128 | expect(mockSetDecorations).toHaveBeenCalledWith(sut.partiallyCoveredLine, []); 129 | expect(mockSetDecorations).toHaveBeenCalledWith(sut.coveredLine, []); 130 | }); 131 | }); 132 | 133 | it('can dispose decorator for all editors', () => { 134 | sut.dispose(); 135 | expect(sut.uncoveredLine.dispose).toHaveBeenCalledTimes(1); 136 | expect(sut.partiallyCoveredLine.dispose).toHaveBeenCalledTimes(1); 137 | expect(sut.coveredLine.dispose).toHaveBeenCalledTimes(1); 138 | }); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /tests/Coverage/Formatters/helpers.test.ts: -------------------------------------------------------------------------------- 1 | jest.unmock('../../../src/Coverage/Formatters/helpers'); 2 | import { isValidPosition, isValidLocation } from '../../../src/Coverage/Formatters/helpers'; 3 | 4 | describe('Coverage Formatters helpers', () => { 5 | describe('isValidPosition()', () => { 6 | it('should return false when the position is falsy', () => { 7 | expect(isValidPosition(undefined)).toBe(false); 8 | }); 9 | 10 | it('should return false when the line number is undefined', () => { 11 | const position: any = {}; 12 | expect(isValidPosition(position)).toBe(false); 13 | }); 14 | 15 | it('should return false when the line number is null', () => { 16 | const position: any = { line: null }; 17 | expect(isValidPosition(position)).toBe(false); 18 | }); 19 | 20 | it('should return false when the line number is less than zero', () => { 21 | const position: any = { line: -1 }; 22 | expect(isValidPosition(position)).toBe(false); 23 | }); 24 | 25 | it('should return false when the line number is zero or more', () => { 26 | let position: any = { line: 0 }; 27 | expect(isValidPosition(position)).toBe(true); 28 | 29 | position = { line: 1 }; 30 | expect(isValidPosition(position)).toBe(true); 31 | }); 32 | }); 33 | 34 | describe('isValidLocation()', () => { 35 | it('should return false when the start is not valid', () => { 36 | const location: any = {}; 37 | expect(isValidLocation(location)).toBe(false); 38 | }); 39 | 40 | it('should return false when the end is not valid', () => { 41 | const location: any = { 42 | start: { line: 2 }, 43 | }; 44 | expect(isValidLocation(location)).toBe(false); 45 | }); 46 | 47 | it('should return true when the start and end positions are valid', () => { 48 | const location: any = { 49 | start: { line: 2 }, 50 | end: { line: 6 }, 51 | }; 52 | expect(isValidLocation(location)).toBe(true); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /tests/JestExt/__snapshots__/outpt-terminal.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`JestOutputTerminal can write output with options can write with option: case 1 1`] = `"regular text"`; 4 | 5 | exports[`JestOutputTerminal can write output with options can write with option: case 2 1`] = ` 6 | "text with newline 7 | 8 | " 9 | `; 10 | 11 | exports[`JestOutputTerminal can write output with options can write with option: case 3 1`] = ` 12 | " 13 | [error] error text 14 | " 15 | `; 16 | 17 | exports[`JestOutputTerminal can write output with options can write with option: case 4 1`] = ` 18 | " 19 | [warn] warning text 20 | " 21 | `; 22 | 23 | exports[`JestOutputTerminal can write output with options can write with option: case 5 1`] = `"bold text"`; 24 | 25 | exports[`JestOutputTerminal can write output with options can write with option: case 6 1`] = ` 26 | " 27 | bold text with newLine 28 | " 29 | `; 30 | 31 | exports[`text format utility function ansiEsc: format by ANSI escape sequence: bold 1`] = `"undefinedwhatever"`; 32 | 33 | exports[`text format utility function ansiEsc: format by ANSI escape sequence: error 1`] = `"undefinedwhatever"`; 34 | 35 | exports[`text format utility function ansiEsc: format by ANSI escape sequence: info 1`] = `"undefinedwhatever"`; 36 | 37 | exports[`text format utility function ansiEsc: format by ANSI escape sequence: lf 1`] = `"undefinedwhatever"`; 38 | 39 | exports[`text format utility function ansiEsc: format by ANSI escape sequence: success 1`] = `"undefinedwhatever"`; 40 | 41 | exports[`text format utility function ansiEsc: format by ANSI escape sequence: warn 1`] = `"undefinedwhatever"`; 42 | 43 | exports[`text format utility function toAnsi: format by output options: ["bold", "new-line"] 1`] = ` 44 | " 45 | a message 46 | " 47 | `; 48 | 49 | exports[`text format utility function toAnsi: format by output options: ["error", "lite"] 1`] = ` 50 | "a message 51 | 52 | " 53 | `; 54 | 55 | exports[`text format utility function toAnsi: format by output options: bold 1`] = `"a message"`; 56 | 57 | exports[`text format utility function toAnsi: format by output options: error 1`] = ` 58 | " 59 | [error] a message 60 | " 61 | `; 62 | 63 | exports[`text format utility function toAnsi: format by output options: info 1`] = ` 64 | " 65 | [info] a message 66 | " 67 | `; 68 | 69 | exports[`text format utility function toAnsi: format by output options: lite 1`] = `"a message"`; 70 | 71 | exports[`text format utility function toAnsi: format by output options: new-line 1`] = ` 72 | " 73 | a message 74 | " 75 | `; 76 | 77 | exports[`text format utility function toAnsi: format by output options: success 1`] = ` 78 | " 79 | [success] a message 80 | " 81 | `; 82 | 83 | exports[`text format utility function toAnsi: format by output options: warn 1`] = ` 84 | " 85 | [warn] a message 86 | " 87 | `; 88 | -------------------------------------------------------------------------------- /tests/JestExt/run-shell.test.ts: -------------------------------------------------------------------------------- 1 | jest.unmock('../../src/JestExt/run-shell'); 2 | 3 | const mockPlatform = jest.fn(); 4 | jest.mock('os', () => ({ platform: mockPlatform })); 5 | jest.mock('path', () => ({ 6 | parse: (p: string) => { 7 | const parts = p.split('/'); 8 | return { base: parts[parts.length - 1] }; 9 | }, 10 | })); 11 | 12 | // import * as vscode from 'vscode'; 13 | import { RunShell, LoginShells } from '../../src/JestExt/run-shell'; 14 | 15 | describe('RunnerShell', () => { 16 | beforeAll(() => { 17 | console.error = jest.fn(); 18 | console.warn = jest.fn(); 19 | }); 20 | beforeEach(() => {}); 21 | 22 | describe('can initialize from a shell setting', () => { 23 | it.each` 24 | case | platform | setting | loginShell | useLoginShell | settingOverride 25 | ${1} | ${'win32'} | ${'c:\\whatever\\powershell'} | ${undefined} | ${'never'} | ${undefined} 26 | ${2} | ${'win32'} | ${undefined} | ${undefined} | ${'never'} | ${undefined} 27 | ${3} | ${'darwin'} | ${'/bin/bash'} | ${LoginShells.bash} | ${false} | ${undefined} 28 | ${4} | ${'darwin'} | ${'/usr/local/bin/zsh'} | ${LoginShells.zsh} | ${false} | ${undefined} 29 | ${5} | ${'darwin'} | ${{ path: '/bin/zsh', args: [] }} | ${LoginShells.zsh} | ${false} | ${'/bin/zsh'} 30 | ${6} | ${'darwin'} | ${{ path: 'bash', args: ['--login'] }} | ${{ path: 'bash', args: ['--login'] }} | ${true} | ${undefined} 31 | ${7} | ${'linux'} | ${undefined} | ${LoginShells.sh} | ${false} | ${undefined} 32 | ${8} | ${'linux'} | ${'whatever'} | ${undefined} | ${'never'} | ${undefined} 33 | ${9} | ${'darwin'} | ${{ path: '/bin/zsh' }} | ${LoginShells.zsh} | ${false} | ${'/bin/zsh'} 34 | `('case $case', ({ platform, setting, loginShell, settingOverride, useLoginShell }) => { 35 | jest.clearAllMocks(); 36 | mockPlatform.mockReturnValue(platform); 37 | const shell = new RunShell(setting); 38 | expect(shell.toSetting()).toEqual(settingOverride ?? setting); 39 | expect(shell.useLoginShell).toEqual(useLoginShell); 40 | 41 | // test loginShell 42 | shell.enableLoginShell(); 43 | if (loginShell) { 44 | expect(shell.toSetting()).toEqual(loginShell); 45 | expect(shell.useLoginShell).toEqual(true); 46 | } else { 47 | expect(shell.useLoginShell).not.toEqual(true); 48 | expect(shell.toSetting()).toEqual(settingOverride ?? setting); 49 | expect(console.warn).toHaveBeenCalled(); 50 | } 51 | }); 52 | }); 53 | it('if setting already a loginShell, will always return the loginShell', () => { 54 | mockPlatform.mockReturnValue('darwin'); 55 | const setting = { path: '/bin/zsh', args: ['-l'] }; 56 | const shell = new RunShell(setting); 57 | expect(shell.useLoginShell).toEqual(true); 58 | expect(shell.toSetting()).toEqual(setting); 59 | 60 | shell.enableLoginShell(); 61 | expect(shell.toSetting()).toEqual(setting); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /tests/JestProcessManagement/test-queue.test.ts: -------------------------------------------------------------------------------- 1 | jest.unmock('../../src/JestProcessManagement/task-queue'); 2 | 3 | import { TaskStatus } from '../../src/JestProcessManagement'; 4 | import { createTaskQueue } from '../../src/JestProcessManagement/task-queue'; 5 | 6 | describe('task-queue', () => { 7 | const mockData = (status: TaskStatus) => ({ 8 | status, 9 | }); 10 | beforeEach(() => { 11 | jest.clearAllMocks(); 12 | }); 13 | it('maxWorker > 0', () => { 14 | expect(() => createTaskQueue('queue-1', 0)).toThrow(); 15 | }); 16 | describe('getRunnableTask', () => { 17 | it.each` 18 | statusList | maxWorker | lastRunnableTaskIdx 19 | ${[]} | ${1} | ${undefined} 20 | ${[]} | ${2} | ${undefined} 21 | ${['pending']} | ${1} | ${0} 22 | ${['pending', 'pending', 'pending']} | ${1} | ${0} 23 | ${['running', 'pending', 'pending']} | ${1} | ${undefined} 24 | ${['running', 'pending', 'pending']} | ${2} | ${1} 25 | ${['running', 'running', 'pending']} | ${2} | ${undefined} 26 | `( 27 | 'task status: $statusList with maxWorker=$maxWorker', 28 | ({ statusList, maxWorker, lastRunnableTaskIdx }) => { 29 | expect.hasAssertions(); 30 | const data = statusList.map((s) => mockData(s)); 31 | const queue = createTaskQueue('queue-1', maxWorker); 32 | queue.add(...data); 33 | // take care of running task 34 | data.forEach((d) => { 35 | if (d.status === 'running') { 36 | expect(queue.getRunnableTask()).not.toBeUndefined(); 37 | } 38 | }); 39 | if (lastRunnableTaskIdx != null) { 40 | expect(queue.getRunnableTask().data).toEqual(data[lastRunnableTaskIdx]); 41 | } else { 42 | expect(queue.getRunnableTask()).toBeUndefined(); 43 | } 44 | } 45 | ); 46 | }); 47 | describe('can add/remove tasks', () => { 48 | const statusList: TaskStatus[] = ['running', 'pending', 'pending']; 49 | const data = statusList.map((s) => mockData(s)); 50 | 51 | it('can add tasks', () => { 52 | const queue = createTaskQueue('queue-1', 1); 53 | queue.add(...data); 54 | expect(queue.map((t) => t.data)).toEqual(data); 55 | expect(queue.size()).toEqual(data.length); 56 | }); 57 | it('can remove tasks', () => { 58 | const queue = createTaskQueue('queue-1', 1); 59 | queue.add(...data); 60 | const tasks = queue.map((t) => t); 61 | queue.remove(tasks[2], tasks[1]); 62 | expect(queue.map((t) => t.data)).toEqual([data[0]]); 63 | }); 64 | it('if no specific task passed in, remove the head of the queue', () => { 65 | const queue = createTaskQueue('queue-1', 1); 66 | queue.add(...data); 67 | queue.remove(); 68 | expect(queue.map((t) => t.data)).toEqual([data[1], data[2]]); 69 | }); 70 | }); 71 | it('can perform map()', () => { 72 | const statusList: TaskStatus[] = ['running', 'pending', 'pending']; 73 | const data = statusList.map((s) => mockData(s)); 74 | const queue = createTaskQueue('queue-1', 1); 75 | queue.add(...data); 76 | expect(queue.map((t) => t.data)).toEqual(data); 77 | }); 78 | it('can perform filter()', () => { 79 | const statusList: TaskStatus[] = ['running', 'running', 'pending']; 80 | const data = statusList.map((s) => mockData(s)); 81 | const queue = createTaskQueue('queue-1', 2); 82 | queue.add(...data); 83 | queue.getRunnableTask(); 84 | queue.getRunnableTask(); 85 | 86 | expect(queue.filter((t) => t.status === 'running').map((t) => t.data)).toEqual([ 87 | data[0], 88 | data[1], 89 | ]); 90 | expect(queue.filter((t) => t.status === 'pending').map((t) => t.data)).toEqual([data[2]]); 91 | }); 92 | it('can perform find()', () => { 93 | const statusList: TaskStatus[] = ['running', 'running', 'running']; 94 | const data = statusList.map((s) => mockData(s)); 95 | const queue = createTaskQueue('queue-1', 3); 96 | queue.add(...data); 97 | queue.getRunnableTask(); 98 | queue.getRunnableTask(); 99 | queue.getRunnableTask(); 100 | 101 | expect(queue.find((t) => t.status === 'running')?.data).toEqual(data[1]); 102 | expect(queue.find((t) => t.status === 'pending')?.data).toEqual(undefined); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /tests/TestResults/match-node.test.ts: -------------------------------------------------------------------------------- 1 | jest.unmock('../../src/TestResults/match-node'); 2 | jest.unmock('../test-helper'); 3 | 4 | import { BaseNode } from '../../src/TestResults/match-node'; 5 | // import * as helper from '../test-helper'; 6 | 7 | describe('BaseNode', () => { 8 | describe('match', () => { 9 | it.each` 10 | attrs1 | attrs2 | options | shouldMatch | addGroup 11 | ${{ fullName: 'x n1' }} | ${{ fullName: 'x n1' }} | ${undefined} | ${true} | ${false} 12 | ${{ fullName: 'x n1' }} | ${{ fullName: 'y n1' }} | ${undefined} | ${false} | ${false} 13 | ${{ fullName: 'x n1', isGroup: 'maybe' }} | ${{ fullName: 'x n1', nonLiteralName: true }} | ${undefined} | ${true} | ${false} 14 | ${{ fullName: 'x n1', isGroup: 'maybe' }} | ${{ fullName: 'y n1', nonLiteralName: true }} | ${undefined} | ${false} | ${false} 15 | ${{ fullName: 'x n1', isGroup: 'maybe' }} | ${{ fullName: 'y n1', nonLiteralName: true }} | ${{ ignoreNonLiteralNameDiff: true }} | ${true} | ${false} 16 | ${{ fullName: 'x n1', isGroup: 'maybe' }} | ${{ fullName: 'y n1' }} | ${{ ignoreNonLiteralNameDiff: true }} | ${false} | ${false} 17 | ${{ fullName: 'x n1' }} | ${{ fullName: 'y n1', nonLiteralName: true }} | ${{ ignoreNonLiteralNameDiff: true }} | ${false} | ${false} 18 | ${{ fullName: 'x n1' }} | ${{ fullName: 'y n1', nonLiteralName: true }} | ${{ ignoreNonLiteralNameDiff: true }} | ${false} | ${true} 19 | ${{ fullName: 'x n1' }} | ${{ fullName: 'y n1', nonLiteralName: true }} | ${{ ignoreNonLiteralNameDiff: true, ignoreGroupDiff: true }} | ${true} | ${true} 20 | `( 21 | 'checks names and groups: $attrs1 and $attrs2 $addGroup => $shouldMatch', 22 | ({ attrs1, attrs2, options, shouldMatch, addGroup }) => { 23 | const n1 = new BaseNode('n1', 10, attrs1); 24 | const n2 = new BaseNode('n2', 20, attrs2); 25 | if (addGroup) { 26 | n1.addGroupMember(new BaseNode('n3', 10)); 27 | } 28 | expect(n1.match(n2, options)).toEqual(shouldMatch); 29 | } 30 | ); 31 | it.each` 32 | loc1 | loc2 | options | shouldMatch 33 | ${[10, 10]} | ${[0, 1]} | ${undefined} | ${true} 34 | ${[10, 10]} | ${[0, 1]} | ${{ checkIsWithin: true }} | ${false} 35 | ${[10, 10]} | ${[0, 100]} | ${undefined} | ${true} 36 | ${[10, 10]} | ${[0, 100]} | ${{ checkIsWithin: true }} | ${true} 37 | ${undefined} | ${undefined} | ${undefined} | ${true} 38 | ${undefined} | ${undefined} | ${{ checkIsWithin: true }} | ${false} 39 | ${undefined} | ${[0, 100]} | ${{ checkIsWithin: true }} | ${false} 40 | ${[10, 10]} | ${undefined} | ${{ checkIsWithin: true }} | ${false} 41 | ${[10, 10]} | ${[10, 10]} | ${{ checkIsWithin: true }} | ${true} 42 | ${[1, 100]} | ${[1, 1]} | ${{ checkIsWithin: true }} | ${false} 43 | `( 44 | 'check location: $loc1 isWithin $loc2? $shouldMatch', 45 | ({ loc1, loc2, options, shouldMatch }) => { 46 | const range1 = loc1 && { 47 | start: { line: loc1[0], column: 0 }, 48 | end: { line: loc1[1], column: 0 }, 49 | }; 50 | const range2 = loc2 && { 51 | start: { line: loc2[0], column: 0 }, 52 | end: { line: loc2[1], column: 0 }, 53 | }; 54 | const n1 = new BaseNode('n1', 10, { 55 | fullName: 'n1', 56 | range: range1, 57 | }); 58 | const n2 = new BaseNode('n1', 20, { 59 | fullName: 'n1', 60 | range: range2, 61 | }); 62 | expect(n1.match(n2, options)).toEqual(shouldMatch); 63 | } 64 | ); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /tests/__snapshots__/helpers.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`toErrorString: arbitrary object 1`] = `"{"text":"anything","value":1}"`; 4 | 5 | exports[`toErrorString: string 1`] = `"regular error"`; 6 | 7 | exports[`toErrorString: undefined 1`] = `""`; 8 | -------------------------------------------------------------------------------- /tests/extension.test.ts: -------------------------------------------------------------------------------- 1 | jest.unmock('../src/extension'); 2 | 3 | const extensionName = 'jest'; 4 | jest.mock('../src/appGlobals', () => ({ 5 | extensionName, 6 | })); 7 | 8 | const statusBar = { 9 | register: jest.fn(() => []), 10 | }; 11 | jest.mock('../src/StatusBar', () => ({ statusBar })); 12 | 13 | const languageProvider = { 14 | register: jest.fn(() => []), 15 | }; 16 | jest.mock('../src/language-provider', () => languageProvider); 17 | 18 | jest.mock('../src/Coverage', () => ({ 19 | registerCoverageCodeLens: jest.fn().mockReturnValue([]), 20 | CoverageCodeLensProvider: jest.fn().mockReturnValue({}), 21 | })); 22 | 23 | jest.mock('../src/test-provider/test-item-context-manager', () => ({ 24 | tiContextManager: { registerCommands: jest.fn(() => []) }, 25 | })); 26 | const mockOutputManager = { 27 | register: jest.fn().mockReturnValue([]), 28 | }; 29 | jest.mock('../src/output-manager', () => ({ 30 | outputManager: mockOutputManager, 31 | })); 32 | 33 | const extensionManager = { 34 | unregisterAllWorkspaces: jest.fn(), 35 | activate: jest.fn(), 36 | register: jest.fn(() => []), 37 | deleteAllExtensions: jest.fn(), 38 | getByName: jest.fn(), 39 | }; 40 | 41 | // tslint:disable-next-line: variable-name 42 | const mockExtensionManager = { 43 | ExtensionManager: jest.fn(() => extensionManager), 44 | getExtensionWindowSettings: jest.fn(() => ({})), 45 | }; 46 | 47 | jest.mock('../src/extension-manager', () => mockExtensionManager); 48 | 49 | import { activate, deactivate } from '../src/extension'; 50 | 51 | describe('Extension', () => { 52 | const context: any = { 53 | subscriptions: { 54 | push: jest.fn(), 55 | }, 56 | }; 57 | beforeEach(() => { 58 | jest.clearAllMocks(); 59 | // ExtensionManager.mockImplementation(() => extensionManager); 60 | }); 61 | describe('activate()', () => { 62 | beforeEach(() => { 63 | context.subscriptions.push.mockReset(); 64 | }); 65 | 66 | it('should instantiate ExtensionManager', () => { 67 | activate(context); 68 | expect(mockExtensionManager.ExtensionManager).toHaveBeenCalledTimes(1); 69 | }); 70 | 71 | it('should register statusBar', () => { 72 | statusBar.register.mockClear(); 73 | activate(context); 74 | expect(statusBar.register).toHaveBeenCalled(); 75 | const [f]: any[] = statusBar.register.mock.calls[0]; 76 | f('whatever'); 77 | expect(extensionManager.getByName).toHaveBeenCalledWith('whatever'); 78 | }); 79 | it('should register language provider', () => { 80 | activate(context); 81 | expect(languageProvider.register).toHaveBeenCalledTimes(1); 82 | }); 83 | it('should register outputManager', () => { 84 | activate(context); 85 | expect(mockOutputManager.register).toHaveBeenCalled(); 86 | }); 87 | }); 88 | 89 | describe('deactivate()', () => { 90 | it('should call unregisterAll on instancesManager', () => { 91 | activate(context); 92 | deactivate(); 93 | expect(extensionManager.deleteAllExtensions).toHaveBeenCalled(); 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /tests/fileMock.ts: -------------------------------------------------------------------------------- 1 | export default ''; 2 | -------------------------------------------------------------------------------- /tests/logging.test.ts: -------------------------------------------------------------------------------- 1 | jest.unmock('../src/logging'); 2 | 3 | import { workspaceLogging } from '../src/logging'; 4 | 5 | describe('workspaceLogging creates a logger factory', () => { 6 | beforeEach(() => { 7 | console.error = jest.fn(); 8 | console.log = jest.fn(); 9 | console.warn = jest.fn(); 10 | }); 11 | it('to apply workspace name for subsequent logger', () => { 12 | const factory = workspaceLogging('workspace-1', true); 13 | const logging = factory.create('child'); 14 | logging('error', 'some error'); 15 | expect(console.error).toHaveBeenCalledWith('[workspace-1/child]', 'some error'); 16 | logging('warn', 'some warning'); 17 | expect(console.warn).toHaveBeenCalledWith('[workspace-1/child]', 'some warning'); 18 | logging('debug', 'some debug message'); 19 | expect(console.log).toHaveBeenCalledWith('[workspace-1/child]', 'some debug message'); 20 | }); 21 | it('to turn on/off debug message for subsequent logger', () => { 22 | let factory = workspaceLogging('workspace-1', true); 23 | let logging = factory.create('child'); 24 | logging('debug', 'some debug message'); 25 | expect(console.log).toHaveBeenCalledWith('[workspace-1/child]', 'some debug message'); 26 | (console.log as jest.Mocked).mockClear(); 27 | 28 | factory = workspaceLogging('workspace-1', false); 29 | logging = factory.create('child'); 30 | logging('debug', 'some debug message'); 31 | expect(console.log).not.toHaveBeenCalled(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /tests/manual-mocks.ts: -------------------------------------------------------------------------------- 1 | // alternative to use __mocks__ under src 2 | 3 | /** 4 | * Jest automock will not mock arrow functions, so we need to mock it manually. 5 | */ 6 | 7 | /* istanbul ignore next */ 8 | jest.mock('../src/output-manager', () => ({ 9 | outputManager: { clearOutputOnRun: jest.fn() }, 10 | })); 11 | 12 | import { mockRun } from './test-provider/test-helper'; 13 | jest.mock('../src/test-provider/jest-test-run', () => { 14 | return { 15 | JestTestRun: jest.fn().mockImplementation((name) => mockRun({}, name)), 16 | }; 17 | }); 18 | -------------------------------------------------------------------------------- /tests/mock-platform.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * help tests to mock the platform related node modules, such as path, os.platform(). 3 | * 4 | * Important: you need to import this module before the module you want to test. 5 | */ 6 | 7 | import * as os from 'os'; 8 | 9 | // Determine the current platform 10 | export const actualPlatform = os.platform(); 11 | 12 | let mockPlatform = actualPlatform; 13 | const platformSpy = jest.spyOn(os, 'platform'); 14 | 15 | jest.mock('path', () => { 16 | const actualPath = jest.requireActual('path'); 17 | 18 | // Return a mock object that dynamically adjusts based on `mockPlatform` 19 | // Create a new object to hold the mock implementation 20 | const pathMock = { 21 | get sep() { 22 | return mockPlatform === 'win32' ? '\\' : '/'; 23 | }, 24 | }; 25 | 26 | // Dynamically add all other methods from the correct platform version 27 | for (const key of Object.keys(actualPath.posix)) { 28 | if (typeof actualPath.posix[key] === 'function') { 29 | pathMock[key] = (...args: any[]) => { 30 | const platformPath = mockPlatform === 'win32' ? actualPath.win32 : actualPath.posix; 31 | return platformPath[key](...args); 32 | }; 33 | } 34 | } 35 | 36 | return pathMock; 37 | }); 38 | 39 | // Utility function to switch the platform in tests 40 | export const setPlatform = (platform: NodeJS.Platform) => { 41 | platformSpy.mockReturnValue(platform); 42 | mockPlatform = platform; 43 | }; 44 | 45 | /* restore the the native platform's path module */ 46 | export const restorePlatform = () => { 47 | setPlatform(actualPlatform); 48 | }; 49 | 50 | //=== original === 51 | 52 | // let mockPlatform = actualPlatform; 53 | // const getMockPlatform = jest.fn().mockReturnValue(actualPlatform); 54 | // const mockSep = jest.fn().mockReturnValue(actualPlatform === 'win32' ? '\\' : '/'); 55 | 56 | // jest.mock('path', () => { 57 | // const actualPath = jest.requireActual('path'); 58 | 59 | // // Return a mock object that dynamically adjusts based on `mockPlatform` 60 | // // Create a new object to hold the mock implementation 61 | // const pathMock = { 62 | // get sep() { 63 | // return mockSep(); 64 | // }, 65 | // }; 66 | 67 | // // Dynamically add all other methods from the correct platform version 68 | // for (const key of Object.keys(actualPath.posix)) { 69 | // if (typeof actualPath.posix[key] === 'function') { 70 | // pathMock[key] = (...args: any[]) => { 71 | // const platformPath = getMockPlatform() === 'win32' ? actualPath.win32 : actualPath.posix; 72 | // return platformPath[key](...args); 73 | // }; 74 | // } 75 | // } 76 | 77 | // return pathMock; 78 | // }); 79 | 80 | // // Utility function to switch the platform in tests 81 | // export const setPlatform = (platform: NodeJS.Platform) => { 82 | // jest.spyOn(os, 'platform').mockReturnValue(platform); 83 | 84 | // getMockPlatform.mockReturnValue(platform); 85 | // mockSep.mockReturnValue(platform === 'win32' ? '\\' : '/'); 86 | // }; 87 | 88 | // /* restore the the native platform's path module */ 89 | // export const restorePlatformPath = () => { 90 | // mockSep.mockReset(); 91 | // setPlatform(actualPlatform); 92 | // mockSep.mockReturnValue(actualPlatform === 'win32' ? '\\' : '/'); 93 | // }; 94 | -------------------------------------------------------------------------------- /tests/quick-fix.test.ts: -------------------------------------------------------------------------------- 1 | jest.unmock('../src/quick-fix'); 2 | import * as vscode from 'vscode'; 3 | import { showQuickFix, QuickFixActionType } from '../src/quick-fix'; 4 | 5 | describe('showQuickFix', () => { 6 | beforeEach(() => { 7 | jest.resetAllMocks(); 8 | vscode.Uri.parse = jest.fn().mockImplementation((s) => s); 9 | }); 10 | 11 | it.each` 12 | actionType | command | args 13 | ${'help'} | ${'vscode.open'} | ${'troubleshooting'} 14 | ${'wizard'} | ${'with-workspace.setup-extension'} | ${['folderName', undefined]} 15 | ${'setup-cmdline'} | ${'with-workspace.setup-extension'} | ${['folderName', { taskId: 'cmdLine' }]} 16 | ${'setup-monorepo'} | ${'with-workspace.setup-extension'} | ${['folderName', { taskId: 'monorepo' }]} 17 | ${'disable-folder'} | ${'with-workspace.disable'} | ${['folderName']} 18 | ${'defer'} | ${'with-workspace.change-run-mode'} | ${['folderName']} 19 | ${'help-long-run'} | ${'vscode.open'} | ${'what-to-do-with-long-running-tests-warning'} 20 | `( 21 | 'select actionType "$actionType" will execute command "$command"', 22 | async ({ actionType, command, args }) => { 23 | expect.hasAssertions(); 24 | 25 | vscode.window.showQuickPick = jest 26 | .fn() 27 | .mockImplementationOnce((items) => Promise.resolve(items[0])); 28 | 29 | await showQuickFix('folderName', [actionType]); 30 | if (Array.isArray(args)) { 31 | expect(vscode.commands.executeCommand).toHaveBeenCalledWith( 32 | expect.stringContaining(command), 33 | ...args 34 | ); 35 | } else { 36 | expect(vscode.commands.executeCommand).toHaveBeenCalledWith( 37 | expect.stringContaining(command), 38 | expect.stringContaining(args) 39 | ); 40 | } 41 | } 42 | ); 43 | it('can display multiple action types and execute the the selected item', async () => { 44 | expect.hasAssertions(); 45 | const actionTypes: QuickFixActionType[] = ['help', 'wizard', 'disable-folder']; 46 | 47 | let items: any[] = []; 48 | vscode.window.showQuickPick = jest.fn().mockImplementationOnce((_items) => { 49 | items = _items; 50 | const wizardItem = items.find((i) => i.label.includes('Customize Extension')); 51 | return Promise.resolve(wizardItem); 52 | }); 53 | 54 | await showQuickFix('a folder', actionTypes); 55 | expect(items).toHaveLength(3); 56 | expect(vscode.commands.executeCommand).toHaveBeenCalledWith( 57 | expect.stringContaining('with-workspace.setup-extension'), 58 | 'a folder', 59 | undefined 60 | ); 61 | }); 62 | 63 | it('should not execute any action if no item is selected', async () => { 64 | expect.hasAssertions(); 65 | vscode.window.showQuickPick = jest.fn().mockResolvedValue(undefined); 66 | 67 | await showQuickFix('whatever', ['help']); 68 | 69 | expect(vscode.commands.executeCommand).not.toHaveBeenCalled(); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /tests/reporter.test.ts: -------------------------------------------------------------------------------- 1 | jest.unmock('../src/reporter'); 2 | // eslint-disable-next-line @typescript-eslint/no-var-requires 3 | const VSCodeJestReporter = require('../src/reporter'); 4 | 5 | describe('VSCodeJest Reporter', () => { 6 | beforeEach(() => { 7 | jest.resetAllMocks(); 8 | process.stderr.write = jest.fn(); 9 | }); 10 | it('reports on RunStart and RunComplete via console.log', () => { 11 | const reporter = new VSCodeJestReporter(); 12 | reporter.onRunStart({} as any); 13 | expect(process.stderr.write).toHaveBeenCalledWith(expect.stringContaining('onRunStart')); 14 | reporter.onRunComplete(new Set(), {} as any); 15 | expect(process.stderr.write).toHaveBeenCalledWith('onRunComplete\r\n'); 16 | }); 17 | describe('report runtime exec error', () => { 18 | it('version >= 29.1.2', () => { 19 | const reporter = new VSCodeJestReporter(); 20 | const args: any = { numTotalTestSuites: 10 }; 21 | reporter.onRunStart(args); 22 | 23 | const result: any = { runExecError: { message: 'some error' } }; 24 | reporter.onRunComplete(new Set(), result); 25 | const output = (process.stderr.write as jest.Mocked).mock.calls[1][0]; 26 | 27 | expect(output).toContain('onRunComplete: execError'); 28 | expect(output).toContain('some error'); 29 | }); 30 | }); 31 | describe('report test error status', () => { 32 | let writeSpy: any; 33 | beforeEach(() => { 34 | writeSpy = jest.spyOn(process.stderr, 'write'); 35 | }); 36 | it('report error if numFailingTests > 0', () => { 37 | const reporter = new VSCodeJestReporter(); 38 | 39 | let testResult: any = { numFailingTests: 0 }; 40 | reporter.onTestFileResult({} as any, testResult, {} as any); 41 | expect(writeSpy).not.toHaveBeenCalled(); 42 | 43 | testResult = { numFailingTests: 1 }; 44 | reporter.onTestFileResult({} as any, testResult, {} as any); 45 | expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('onTestFileResult')); 46 | }); 47 | it('report error if there is execError > 0', () => { 48 | const reporter = new VSCodeJestReporter(); 49 | 50 | let testResult: any = {}; 51 | reporter.onTestFileResult({} as any, testResult, {} as any); 52 | expect(writeSpy).not.toHaveBeenCalled(); 53 | 54 | testResult = { testExecError: {} }; 55 | reporter.onTestFileResult({} as any, testResult, {} as any); 56 | expect(writeSpy).toHaveBeenCalledWith(expect.stringContaining('onTestFileResult')); 57 | }); 58 | }); 59 | 60 | it('getLastError never returns error', () => { 61 | const reporter = new VSCodeJestReporter(); 62 | expect(reporter.getLastError()).toBeUndefined(); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /tests/setup-wizard/tasks/task-test-helper.ts: -------------------------------------------------------------------------------- 1 | import { makeWorkspaceFolder } from '../../test-helper'; 2 | import * as path from 'path'; 3 | 4 | export const createWizardContext = (debugConfigProvider: any, wsName?: string): any => ({ 5 | debugConfigProvider, 6 | wsManager: { 7 | getValidWorkspaceFolders: jest.fn(), 8 | getFoldersFromFilesystem: jest.fn(), 9 | }, 10 | vscodeContext: { 11 | globalState: { 12 | get: jest.fn(), 13 | update: jest.fn(), 14 | }, 15 | workspaceState: { 16 | get: jest.fn(), 17 | update: jest.fn(), 18 | }, 19 | }, 20 | workspace: wsName ? makeWorkspaceFolder(wsName) : undefined, 21 | message: jest.fn(), 22 | }); 23 | 24 | export const validateTaskConfigUpdate = ( 25 | mockSaveConfig: jest.Mocked, 26 | key: string, 27 | callBack?: (value?: T) => void 28 | ): any => { 29 | if (!callBack) { 30 | expect(mockSaveConfig).not.toHaveBeenCalled(); 31 | return; 32 | } 33 | const entries = mockSaveConfig.mock.calls[0]; 34 | let called = false; 35 | entries.forEach((entry) => { 36 | const { name, value } = entry; 37 | if (name === key) { 38 | callBack(value); 39 | called = true; 40 | } 41 | }); 42 | if (!called) { 43 | callBack(); 44 | } 45 | }; 46 | 47 | export const toUri = (...pathParts: string[]): any => ({ 48 | fsPath: path.join(...pathParts), 49 | path: pathParts.join('/'), 50 | }); 51 | -------------------------------------------------------------------------------- /tests/setup-wizard/test-helper.ts: -------------------------------------------------------------------------------- 1 | import { ActionMessageType } from '../../src/setup-wizard/types'; 2 | 3 | export const mockWizardHelper = (mockHelper: jest.Mocked): any => { 4 | const mockShowActionMenu = (...ids: number[]) => { 5 | ids.forEach((id) => { 6 | mockHelper.showActionMenu.mockImplementationOnce((items) => { 7 | for (const item of items) { 8 | if (item.id === id) { 9 | return item.action(); 10 | } 11 | const found = item.buttons?.find((b) => b?.id === id); 12 | if (found) { 13 | return found.action(); 14 | } 15 | } 16 | }); 17 | }); 18 | }; 19 | 20 | const mockShowActionMessage = (msgType: ActionMessageType, id: number) => { 21 | mockHelper.showActionMessage.mockImplementation((type, _b, ...buttons) => { 22 | if (type === msgType) { 23 | return buttons?.find((b) => b.id === id)?.action?.(); 24 | } 25 | }); 26 | }; 27 | 28 | const mockHelperSetup = () => { 29 | mockHelper.actionItem.mockImplementation((id, label, detail, action) => ({ 30 | id, 31 | label, 32 | detail, 33 | action, 34 | })); 35 | }; 36 | const mockSelectWorkspace = (ws?: string) => { 37 | mockHelper.selectWorkspaceFolder.mockImplementation(() => Promise.resolve(ws)); 38 | }; 39 | return { 40 | mockShowActionMenu, 41 | mockShowActionMessage, 42 | mockHelperSetup, 43 | mockSelectWorkspace, 44 | }; 45 | }; 46 | 47 | export const throwError = (msg: string): void => { 48 | throw new Error(msg); 49 | }; 50 | -------------------------------------------------------------------------------- /tests/terminal-link-provider.test.ts: -------------------------------------------------------------------------------- 1 | jest.unmock('../src/terminal-link-provider'); 2 | 3 | import * as vscode from 'vscode'; 4 | import { 5 | ExecutableTerminalLinkProvider, 6 | ExecutableLinkScheme, 7 | } from '../src/terminal-link-provider'; 8 | 9 | describe('ExecutableTerminalLinkProvider', () => { 10 | let provider: ExecutableTerminalLinkProvider; 11 | 12 | beforeEach(() => { 13 | provider = new ExecutableTerminalLinkProvider(); 14 | }); 15 | 16 | afterEach(() => { 17 | jest.restoreAllMocks(); 18 | }); 19 | 20 | it('register', () => { 21 | vscode.window.registerTerminalLinkProvider = jest.fn().mockReturnValueOnce('disposable'); 22 | expect(provider.register()).toEqual('disposable'); 23 | expect(vscode.window.registerTerminalLinkProvider).toHaveBeenCalledWith(provider); 24 | }); 25 | describe('handleTerminalLink', () => { 26 | it('should execute the command with the correct arguments', async () => { 27 | const link: any = { 28 | data: 'whatever', 29 | }; 30 | // with args 31 | vscode.Uri.parse = jest.fn().mockReturnValueOnce({ 32 | authority: 'folderName', 33 | path: '/command', 34 | query: encodeURIComponent(JSON.stringify({ arg1: 'value1', arg2: 'value2' })), 35 | }); 36 | await provider.handleTerminalLink(link); 37 | expect(vscode.commands.executeCommand).toHaveBeenCalledWith('command', 'folderName', { 38 | arg1: 'value1', 39 | arg2: 'value2', 40 | }); 41 | 42 | // without args 43 | vscode.Uri.parse = jest.fn().mockReturnValueOnce({ 44 | authority: 'folderName', 45 | path: '/command', 46 | }); 47 | await provider.handleTerminalLink(link); 48 | expect(vscode.commands.executeCommand).toHaveBeenCalledWith( 49 | 'command', 50 | 'folderName', 51 | undefined 52 | ); 53 | }); 54 | 55 | it('should show an error message if the link cannot be parsed', async () => { 56 | const link: any = { 57 | data: 'whatever', 58 | }; 59 | vscode.Uri.parse = jest.fn().mockImplementationOnce(() => { 60 | throw new Error('uri parse error'); 61 | }); 62 | await provider.handleTerminalLink(link); 63 | 64 | expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( 65 | expect.stringContaining('Failed to handle link "whatever"') 66 | ); 67 | }); 68 | }); 69 | 70 | describe('provideTerminalLinks', () => { 71 | it('should return an empty array if there are no links in the line', () => { 72 | const context: any = { 73 | line: 'some text without links', 74 | }; 75 | const links = provider.provideTerminalLinks(context, undefined); 76 | expect(links).toEqual([]); 77 | }); 78 | 79 | it('should return an array of links if there are links in the line', () => { 80 | const context: any = { 81 | line: `some text with a link ${ExecutableLinkScheme}://folderName/command?${encodeURIComponent( 82 | JSON.stringify({ arg1: 'value1', arg2: 'value2' }) 83 | )} and another link ${ExecutableLinkScheme}://folderName/other-command`, 84 | }; 85 | const links = provider.provideTerminalLinks(context, undefined); 86 | expect(links).toEqual([ 87 | { 88 | startIndex: 22, 89 | length: 92, 90 | tooltip: 'execute command', 91 | data: `${ExecutableLinkScheme}://folderName/command?${encodeURIComponent( 92 | JSON.stringify({ arg1: 'value1', arg2: 'value2' }) 93 | )}`, 94 | }, 95 | { 96 | startIndex: 132, 97 | length: 38, 98 | tooltip: 'execute command', 99 | data: `${ExecutableLinkScheme}://folderName/other-command`, 100 | }, 101 | ]); 102 | }); 103 | it('would returns empty array when match encountered error', () => { 104 | const originalMatchAll = String.prototype.matchAll; 105 | String.prototype.matchAll = jest.fn().mockReturnValueOnce([{ index: undefined }]); 106 | const context: any = { 107 | line: `some text with a link ${ExecutableLinkScheme}://folderName/command`, 108 | }; 109 | const links = provider.provideTerminalLinks(context, undefined); 110 | expect(links).toEqual([]); 111 | 112 | String.prototype.matchAll = originalMatchAll; 113 | }); 114 | }); 115 | 116 | describe('executableLink', () => { 117 | it.each` 118 | seq | folderName | command | args | expectedPath 119 | ${1} | ${'folderName'} | ${'command'} | ${undefined} | ${'//folderName/command'} 120 | ${2} | ${'folderName'} | ${'command'} | ${{ arg1: 'value 1', arg2: 'value 2' }} | ${'//folderName/command'} 121 | ${3} | ${'folder name'} | ${'command-1'} | ${{ arg1: 'value 1', arg2: 'value 2' }} | ${'//folder%20name/command-1'} 122 | `('case $seq: returns an executable link', ({ folderName, command, args, expectedPath }) => { 123 | const link = provider.executableLink(folderName, command, args); 124 | if (args) { 125 | const encodedArgs = encodeURIComponent(JSON.stringify(args)); 126 | expect(link).toEqual(`${ExecutableLinkScheme}:${expectedPath}?${encodedArgs}`); 127 | } else { 128 | expect(link).toEqual(`${ExecutableLinkScheme}:${expectedPath}`); 129 | } 130 | }); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /tests/test-provider/test-helper.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { mockJestExtEvents } from '../test-helper'; 3 | import { RunMode } from '../../src/JestExt/run-mode'; 4 | 5 | export class TestItemCollectionMock { 6 | constructor(public parent?: vscode.TestItem) {} 7 | private items: vscode.TestItem[] = []; 8 | get size(): number { 9 | return this.items.length; 10 | } 11 | replace = (list: vscode.TestItem[]): void => { 12 | this.items = list; 13 | }; 14 | get = (id: string): vscode.TestItem | undefined => this.items.find((i) => i.id === id); 15 | add = (item: vscode.TestItem): void => { 16 | this.items.push(item); 17 | (item as any).parent = this.parent; 18 | }; 19 | delete = (id: string): void => { 20 | this.items = this.items.filter((i) => i.id !== id); 21 | }; 22 | forEach = (f: (item: vscode.TestItem) => void): void => { 23 | this.items.forEach(f); 24 | }; 25 | } 26 | 27 | export const mockExtExplorerContext = (wsName = 'ws-1', override: any = {}): any => { 28 | return { 29 | loggingFactory: { create: jest.fn().mockReturnValue(jest.fn()) }, 30 | session: { scheduleProcess: jest.fn() }, 31 | workspace: { name: wsName, uri: { fsPath: `/${wsName}` } }, 32 | testResultProvider: { 33 | events: { 34 | testListUpdated: { event: jest.fn().mockReturnValue({ dispose: jest.fn() }) }, 35 | testSuiteChanged: { event: jest.fn().mockReturnValue({ dispose: jest.fn() }) }, 36 | }, 37 | getTestList: jest.fn().mockReturnValue([]), 38 | isTestFile: jest.fn().mockReturnValue('yes'), 39 | getTestSuiteResult: jest.fn().mockReturnValue({}), 40 | previewSnapshot: jest.fn(), 41 | }, 42 | debugTests: jest.fn(), 43 | sessionEvents: mockJestExtEvents(), 44 | settings: { testExplorer: { enabled: true }, runMode: new RunMode() }, 45 | output: { write: jest.fn(), dispose: jest.fn(), clear: jest.fn() }, 46 | ...override, 47 | }; 48 | }; 49 | 50 | export const mockRun = (request?: any, name?: any): any => ({ 51 | request, 52 | name, 53 | started: jest.fn(), 54 | passed: jest.fn(), 55 | skipped: jest.fn(), 56 | errored: jest.fn(), 57 | failed: jest.fn(), 58 | enqueued: jest.fn(), 59 | appendOutput: jest.fn(), 60 | end: jest.fn(), 61 | cancel: jest.fn(), 62 | write: jest.fn(), 63 | addProcess: jest.fn(), 64 | updateRequest: jest.fn(), 65 | token: { onCancellationRequested: jest.fn() }, 66 | }); 67 | export const mockController = (): any => { 68 | const runMocks = []; 69 | return { 70 | runMocks, 71 | lastRunMock: () => (runMocks.length > 0 ? runMocks[runMocks.length - 1] : undefined), 72 | createTestRun: jest.fn().mockImplementation((r, n) => { 73 | const run = mockRun(r, n); 74 | runMocks.push(run); 75 | return run; 76 | }), 77 | dispose: jest.fn(), 78 | createRunProfile: jest.fn().mockImplementation((label, kind, runHandler, isDefault, tag) => ({ 79 | label, 80 | kind, 81 | runHandler, 82 | isDefault, 83 | tag, 84 | })), 85 | createTestItem: jest.fn().mockImplementation((id, label, uri) => { 86 | const item: any = { 87 | id, 88 | label, 89 | uri, 90 | errored: jest.fn(), 91 | tags: [], 92 | }; 93 | item.children = new TestItemCollectionMock(item); 94 | return item; 95 | }), 96 | items: new TestItemCollectionMock(), 97 | }; 98 | }; 99 | 100 | export const mockJestProcess = (id: string, extra?: any): any => { 101 | return { 102 | id, 103 | start: jest.fn(), 104 | stop: jest.fn(), 105 | status: 'pending', 106 | ...(extra ?? {}), 107 | }; 108 | }; 109 | -------------------------------------------------------------------------------- /tests/test-provider/test-provider-context.test.ts: -------------------------------------------------------------------------------- 1 | import '../manual-mocks'; 2 | 3 | jest.unmock('../../src/test-provider/test-provider-context'); 4 | jest.unmock('./test-helper'); 5 | 6 | import * as vscode from 'vscode'; 7 | import { JestTestProviderContext } from '../../src/test-provider/test-provider-context'; 8 | import { JestTestRun } from '../../src/test-provider/jest-test-run'; 9 | 10 | describe('JestTestProviderContext', () => { 11 | it('when try to getTag not in any profiles, throw error', () => { 12 | const whatever: any = {}; 13 | const profile: any = { tag: { id: 'run' } }; 14 | const context = new JestTestProviderContext(whatever, whatever, [profile]); 15 | expect(context.getTag('run')).toEqual(profile.tag); 16 | expect(() => context.getTag('debug')).toThrow(); 17 | }); 18 | it('createTestRun should create a JIT JestTestRun', () => { 19 | const extContext: any = {}; 20 | const mockRun: any = { appendOutput: jest.fn() }; 21 | const mockController: any = { createTestRun: jest.fn().mockReturnValue(mockRun) }; 22 | const profile: any = { tag: { id: 'run' } }; 23 | const context = new JestTestProviderContext(extContext, mockController, [profile]); 24 | const request: any = {}; 25 | 26 | const mockJestRun: any = {}; 27 | (JestTestRun as jest.Mocked) = jest.fn().mockReturnValue(mockJestRun); 28 | const jestRun = context.createTestRun(request, { name: 'test-run' }); 29 | 30 | expect(jestRun).toBe(mockJestRun); 31 | expect(JestTestRun).toHaveBeenCalledWith('test-run', context, request, expect.anything()); 32 | // no vscode run should be created yet 33 | expect(mockController.createTestRun).not.toHaveBeenCalled(); 34 | 35 | // vscode run will be created through the factory function 36 | const factory = (JestTestRun as jest.Mocked).mock.calls[0][3]; 37 | const run = factory(request, 'new-test-run'); 38 | 39 | expect(mockController.createTestRun).toHaveBeenCalledWith(request, 'new-test-run'); 40 | expect(run).toBe(mockRun); 41 | }); 42 | describe('requestFrom', () => { 43 | let context: JestTestProviderContext; 44 | let mockCollection; 45 | 46 | const makeCollection = (items: any[]) => { 47 | const collection: any = { 48 | items: items ?? [], 49 | get: (id) => collection.items.find((i) => i.id === id), 50 | forEach: (callback) => { 51 | collection.items.forEach(callback); 52 | }, 53 | }; 54 | return collection; 55 | }; 56 | const makeItem = (id: string, children?: any[]) => ({ id, children: makeCollection(children) }); 57 | 58 | beforeEach(() => { 59 | jest.resetAllMocks(); 60 | 61 | (vscode.TestRunRequest as jest.Mocked) = jest.fn((include, exclude, profile) => ({ 62 | include, 63 | exclude, 64 | profile, 65 | })); 66 | 67 | const item1 = makeItem('id1', [makeItem('id1-1'), makeItem('id1-2')]); 68 | const item2 = makeItem('id2'); 69 | mockCollection = makeCollection([item1, item2]); 70 | 71 | const controller: any = { items: mockCollection }; 72 | const profiles: any[] = [{ label: 'test' }]; 73 | context = new JestTestProviderContext({} as any, controller, profiles); 74 | }); 75 | 76 | it('should return a new request with included items found in the controller', () => { 77 | const item1 = makeItem('id1-2'); 78 | const item2 = makeItem('id2'); 79 | 80 | const request: any = { include: [item1, item2], profile: { label: 'test' } }; 81 | const newRequest = context.requestFrom(request); 82 | expect(newRequest.include?.map((i) => i.id)).toEqual(['id1-2', 'id2']); 83 | expect(newRequest.exclude).toBeUndefined(); 84 | expect(newRequest.profile.label).toBe('test'); 85 | expect(newRequest).not.toBe(request); 86 | }); 87 | 88 | it('should throw an error if an included item is not found in the controller', () => { 89 | const item1 = makeItem('id3'); 90 | 91 | const request: any = { include: [item1], profile: { label: 'test' } }; 92 | expect(() => context.requestFrom(request)).toThrow('failed to find item'); 93 | }); 94 | 95 | it('should return a new request with excluded items found in the controller', () => { 96 | const item1 = makeItem('id1'); 97 | const item2 = makeItem('id1-2'); 98 | 99 | const request: any = { include: [item1], exclude: [item2], profile: { label: 'test' } }; 100 | const newRequest = context.requestFrom(request); 101 | expect(newRequest.include?.map((i) => i.id)).toEqual(['id1']); 102 | expect(newRequest.exclude?.map((i) => i.id)).toEqual(['id1-2']); 103 | expect(newRequest.profile.label).toBe('test'); 104 | expect(newRequest).not.toBe(request); 105 | }); 106 | 107 | it('should throw an error if an excluded item is not found in the controller', () => { 108 | const item1 = makeItem('id1'); 109 | const item2 = makeItem('id1-3'); 110 | 111 | const request: any = { include: [item1], exclude: [item2], profile: { label: 'test' } }; 112 | expect(() => context.requestFrom(request)).toThrow('failed to find item'); 113 | }); 114 | 115 | it('should throw an error if the profile is not found in the context', () => { 116 | const item1 = makeItem('id1'); 117 | 118 | const request: any = { include: [item1], profile: { label: 'new-profile' } }; 119 | expect(() => context.requestFrom(request)).toThrow('failed to find profile'); 120 | }); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "strict": true, 5 | "noImplicitAny": false, 6 | "strictNullChecks": false, 7 | }, 8 | "exclude": ["../__mocks__"] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "noUnusedLocals": true, 7 | "allowSyntheticDefaultImports": true, 8 | "noUnusedParameters": true, 9 | "target": "es6", 10 | "outDir": "out", 11 | "lib": ["es6", "es7", "es2020", "es2021"], 12 | "sourceMap": true, 13 | "rootDir": ".", 14 | "plugins": [ 15 | { 16 | "name": "typescript-tslint-plugin", 17 | "alwaysShowRuleFailuresAsWarnings": false, 18 | "ignoreDefinitionFiles": true, 19 | "suppressWhileTypeErrorsPresent": false, 20 | "mockTypeScriptVersion": false 21 | } 22 | ] 23 | }, 24 | "include": ["**/*"], 25 | "exclude": ["__mocks__", "node_modules", "tests"] 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, // Disable source maps for production 5 | "declaration": false, // Disable declaration files generation 6 | "removeComments": true, // Optional: Remove comments in the production build 7 | "stripInternal": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /typings/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const content: string; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /webpack/dummy-module.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A dummy module to replace unnecessarily required packages. 3 | * 4 | * The dynamic nature of this module ensures that any property access will be 5 | * handled gracefully, throwing an error to indicate that the specific functionality 6 | * is not available. This approach avoids the need to hard-code specific properties 7 | * or methods, making the setup more robust against changes in the source code. 8 | */ 9 | 10 | const createThrowingProxy = (name) => { 11 | return new Proxy(function () {}, { 12 | apply: function (_target, _thisArg, _argumentsList) { 13 | console.log(`Calling function ${name}`); 14 | throw new Error(`${name} is not available.`); 15 | }, 16 | get: function (_target, prop) { 17 | console.log(`Accessing property ${prop.toString()} on ${name}`); 18 | if (prop === 'default') { 19 | return () => { 20 | console.log(`Accessing default export of ${name}`); 21 | throw new Error('The module is not available.'); 22 | }; 23 | } 24 | if (prop === 'keys' || prop === 'values' || prop === 'entries') { 25 | return () => { 26 | console.log(`Accessing Object.${prop.toString()} on ${name}`); 27 | throw new Error(`Object.${prop.toString()} is not available.`); 28 | }; 29 | } 30 | // Handle `types` object for @babel/core specifically 31 | if (name === 'dummy-module' && prop === 'types') { 32 | return new Proxy( 33 | {}, 34 | { 35 | get: function (_target, prop) { 36 | console.log(`Accessing types.${prop.toString()} on ${name}`); 37 | return () => { 38 | throw new Error(`types.${prop.toString()} is not available.`); 39 | }; 40 | }, 41 | } 42 | ); 43 | } 44 | return createThrowingProxy(`${name}.${prop.toString()}`); 45 | }, 46 | set: function (_target, _prop, _value) { 47 | return true; // Allow setting properties without errors 48 | }, 49 | has: function (_target, _prop) { 50 | return true; // Indicate that any property exists 51 | }, 52 | getPrototypeOf: function (_target) { 53 | return Object.prototype; 54 | }, 55 | }); 56 | }; 57 | 58 | // Create a proxy to dynamically handle property access on the dummy module 59 | module.exports = createThrowingProxy('dummy-module'); 60 | -------------------------------------------------------------------------------- /webpack/jest-snapshot-loader.js: -------------------------------------------------------------------------------- 1 | const loaderUtils = require('loader-utils'); 2 | 3 | /** 4 | * A custom Webpack loader to strip unnecessary dependencies within the jest-snapshot package. 5 | * 6 | * This loader targets two main loading scenarios: 7 | * 1. Dynamic require calls, aka "requireOutside", for specific packages that are not necessary for our use 8 | * but may cause runtime errors if they are missing. 9 | * 2. Direct require calls for specific packages that are not necessary for our use but may cause runtime errors if they are missing. 10 | * 11 | * [Motivation]: 12 | * Since the jest-snapshot package (in v30) is already Webpack bundled, normal Webpack parsing/alias resolution will not work. 13 | * Therefore, we use this loader to modify the "source" code before it's processed by Webpack. 14 | * 15 | * A related jest issue: https://github.com/facebook/jest/issues/11894 regarding requireOutside. 16 | * 17 | * @param {string} source - The source code to be transformed. 18 | */ 19 | module.exports = function (source) { 20 | this.cacheable(); 21 | const options = loaderUtils.getOptions(this); 22 | const replacements = options.replacements; 23 | 24 | let replacedSource = source; 25 | 26 | replacements.forEach(({ packageName, replacement }) => { 27 | const regex = new RegExp( 28 | `require\\(require\\.resolve\\(['"]${packageName}['"],\\s*{[^}]*}\\)\\)`, 29 | 'g' 30 | ); 31 | if (regex.test(replacedSource)) { 32 | replacedSource = replacedSource.replace(regex, `require('${replacement}')`); 33 | } 34 | 35 | // Also replace direct require statements 36 | const directRequireRegex = new RegExp(`__webpack_require__\\(['"]${packageName}['"]\\)`, 'g'); 37 | if (directRequireRegex.test(replacedSource)) { 38 | replacedSource = replacedSource.replace(directRequireRegex, `require('${replacement}')`); 39 | } 40 | }); 41 | 42 | return replacedSource; 43 | }; 44 | -------------------------------------------------------------------------------- /webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const glob = require('glob'); 5 | 6 | /**@returns {import('webpack').Configuration}*/ 7 | module.exports = (env) => { 8 | /**@type {any} */ 9 | const externals = [ 10 | { 'jest-config': 'root {}' }, // the jest-config module isn't utilized in this plugin, compiling it would result in unnecessary overhead and errors 11 | { vscode: 'commonjs vscode' }, // the vscode-module is created on-the-fly and must be excluded. 12 | 'fsevents', // extension will not need to do any 'watch' directly, no need for this library 13 | 'typescript', 14 | ]; 15 | 16 | // Function to find files matching a pattern within a specific package 17 | function addMatchingFiles(packageName, filePattern) { 18 | const files = glob.sync(`node_modules/**/${packageName}/${filePattern}`, { absolute: true }); 19 | const normalizedFiles = files.map((file) => path.normalize(file)); 20 | return normalizedFiles; 21 | } 22 | 23 | // this path should always use forward slashes. On windows, this requires replacing backslashes with forward slashes 24 | const dummyModulePath = path.resolve(__dirname, 'dummy-module.js').replace(/\\/g, '/'); 25 | 26 | const replacements = [ 27 | { packageName: '@babel/generator', replacement: dummyModulePath }, 28 | { packageName: '@babel/core', replacement: dummyModulePath }, 29 | { 30 | packageName: './src/InlineSnapshots.ts', 31 | replacement: dummyModulePath, 32 | }, 33 | ]; 34 | 35 | const tsConfigFile = env.production ? 'tsconfig.prod.json' : 'tsconfig.json'; 36 | 37 | return { 38 | context: path.resolve(__dirname, '..'), // Adjusted to point to the root of the project 39 | target: 'node', 40 | entry: { 41 | extension: path.resolve(__dirname, '../src/extension.ts'), 42 | reporter: path.resolve(__dirname, '../src/reporter.ts'), 43 | }, 44 | output: { 45 | path: path.resolve(__dirname, '../out'), // Adjusted to ensure output is correct 46 | filename: '[name].js', 47 | libraryTarget: 'commonjs2', 48 | devtoolModuleFilenameTemplate: '../[resource-path]', 49 | }, 50 | devtool: 'source-map', 51 | externals, 52 | resolve: { 53 | extensions: ['.ts', '.js'], 54 | alias: { 55 | '@jest/transform': false, 56 | 'babel-preset-current-node-syntax': false, 57 | }, 58 | }, 59 | module: { 60 | rules: [ 61 | { 62 | test: /\.ts$/, 63 | exclude: /node_modules/, 64 | use: [ 65 | { 66 | loader: 'ts-loader', 67 | options: { 68 | configFile: path.resolve(__dirname, `../${tsConfigFile}`), 69 | }, 70 | }, 71 | ], 72 | }, 73 | { 74 | test: /\.svg$/, 75 | use: [{ loader: 'raw-loader' }], 76 | }, 77 | { 78 | test: /\.js$/, 79 | include: [...addMatchingFiles('jest-snapshot', '**/*.js')], 80 | use: [ 81 | { 82 | loader: path.resolve(__dirname, './jest-snapshot-loader.js'), 83 | options: { replacements }, 84 | }, 85 | ], 86 | }, 87 | ], 88 | }, 89 | }; 90 | }; 91 | --------------------------------------------------------------------------------