├── .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 |
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 | [0;31m[error] error text[0m
14 | "
15 | `;
16 |
17 | exports[`JestOutputTerminal can write output with options can write with option: case 4 1`] = `
18 | "
19 | [0;33m[warn] warning text[0m
20 | "
21 | `;
22 |
23 | exports[`JestOutputTerminal can write output with options can write with option: case 5 1`] = `"[1mbold text[0m"`;
24 |
25 | exports[`JestOutputTerminal can write output with options can write with option: case 6 1`] = `
26 | "
27 | [1mbold text with newLine[0m
28 | "
29 | `;
30 |
31 | exports[`text format utility function ansiEsc: format by ANSI escape sequence: bold 1`] = `"undefinedwhatever[0m"`;
32 |
33 | exports[`text format utility function ansiEsc: format by ANSI escape sequence: error 1`] = `"undefinedwhatever[0m"`;
34 |
35 | exports[`text format utility function ansiEsc: format by ANSI escape sequence: info 1`] = `"undefinedwhatever[0m"`;
36 |
37 | exports[`text format utility function ansiEsc: format by ANSI escape sequence: lf 1`] = `"undefinedwhatever[0m"`;
38 |
39 | exports[`text format utility function ansiEsc: format by ANSI escape sequence: success 1`] = `"undefinedwhatever[0m"`;
40 |
41 | exports[`text format utility function ansiEsc: format by ANSI escape sequence: warn 1`] = `"undefinedwhatever[0m"`;
42 |
43 | exports[`text format utility function toAnsi: format by output options: ["bold", "new-line"] 1`] = `
44 | "
45 | [1ma message[0m
46 | "
47 | `;
48 |
49 | exports[`text format utility function toAnsi: format by output options: ["error", "lite"] 1`] = `
50 | "[0;31ma message[0m
51 |
52 | "
53 | `;
54 |
55 | exports[`text format utility function toAnsi: format by output options: bold 1`] = `"[1ma message[0m"`;
56 |
57 | exports[`text format utility function toAnsi: format by output options: error 1`] = `
58 | "
59 | [0;31m[error] a message[0m
60 | "
61 | `;
62 |
63 | exports[`text format utility function toAnsi: format by output options: info 1`] = `
64 | "
65 | [0;34m[info] a message[0m
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 | [0;32m[success] a message[0m
80 | "
81 | `;
82 |
83 | exports[`text format utility function toAnsi: format by output options: warn 1`] = `
84 | "
85 | [0;33m[warn] a message[0m
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 |
--------------------------------------------------------------------------------