jest-environment-vscode-extension v0.0.5
jest-environment-vscode-extension
🎪 The best way to run and write tests for your VSCode extension
Key Features:
- run tests using Jest
- built-in API making tests simpler to write and read
- zero JS configuration
Setup
1 - Install the following packages:
npm install jest jest-environment-vscode-extension @types/jest @types/jest-environment-vscode-extension --save-dev
2 - On .vscode/tasks.json
, add the following within the tasks
array:
{
"label": "create-test-workspace-folder",
"type": "shell",
"command": "mkdir",
"args": ["-p", "test-workspace"],
"presentation": {
"reveal": "silent",
"revealProblems": "onProblem"
}
},
{
"label": "remove-test-workspace-folder",
"type": "shell",
"command": "rm",
"args": ["-rf", "test-workspace"],
"presentation": {
"reveal": "silent",
"revealProblems": "onProblem"
}
},
{
"label": "insert-monkey-patch-allow-mocks",
"type": "shell",
"command": "node ./node_modules/.bin/insert-monkey-patch-allow-mocks ${workspaceFolder}",
"presentation": {
"reveal": "silent",
"revealProblems": "onProblem"
},
},
{
"label": "drop-monkey-patch-allow-mocks",
"type": "shell",
"command": "node ./node_modules/.bin/drop-monkey-patch-allow-mocks ${workspaceFolder}",
"presentation": {
"reveal": "silent",
"revealProblems": "onProblem"
}
},
{
"label": "pre-run-tests",
"dependsOrder": "sequence",
"dependsOn": [
"remove-test-workspace-folder",
"create-test-workspace-folder",
"build",
"insert-monkey-patch-allow-mocks"
],
"presentation": {
"reveal": "silent",
"revealProblems": "onProblem"
}
},
{
"label": "post-run-tests",
"dependsOn": [
"remove-test-workspace-folder",
"drop-monkey-patch-allow-mocks"
],
"presentation": {
"reveal": "silent",
"revealProblems": "onProblem"
}
}
3 - On .vscode/launch.json
, add the following within the configurations
array:
{
"name": "Test Extension - No Workspace",
"preLaunchTask": "pre-run-tests",
"postDebugTask": "post-run-tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"/no-workspace",
"--disable-extensions",
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/node_modules/.bin/vscode-tests-runner"
],
"env": {
"VSCODE_TESTS_PATH": "${workspaceFolder}/out/tests/no-workspace/"
},
"outFiles": ["${workspaceFolder}/out/tests/**/*.js"]
},
{
"name": "Test Extension - With Workspace",
"preLaunchTask": "pre-run-tests",
"postDebugTask": "post-run-tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"${workspaceFolder}/test-workspace",
"--disable-extensions",
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/node_modules/.bin/vscode-tests-runner"
],
"env": {
"VSCODE_TESTS_PATH": "${workspaceFolder}/out/tests/with-workspace/"
},
"outFiles": ["${workspaceFolder}/out/tests/**/*.js"]
}
4 - Now, write your tests that depend on a workspace within tests/with-workspace
. And if it doesn't need it, you can write them within tests/no-workspace
.
Setup finished! 🎉
Now you can run the tests using VSCode:
Running on CI
Running by VSCode is great for development since it's quick and can use breakpoints. But we need to do one more step to can run on CI.
1 - On package.json
, add the following within the scripts
object:
"tests:ci:no-workspace": "vscode-electron-starter no-workspace insiders out/tests/no-workspace",
"tests:ci:with-workspace": "vscode-electron-starter with-workspace insiders out/tests/with-workspace"
The penultimate parameter is the VSCode version being used. You can use stable
, insiders
, or a version number (e.g., 1.32.0
). The last parameter is the path of the test.
2 - Now you can call these scripts on CI. Following, a script to run on GitHub actions:
on:
push:
branches:
jobs:
test:
name: Test
strategy:
matrix:
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: 16
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
- name: Run test - No workspace
uses: GabrielBB/xvfb-action@v1.0
with:
run: npm run tests:ci:no-workspace
- name: Run test - With workspace
uses: GabrielBB/xvfb-action@v1.0
with:
run: npm run tests:ci:with-workspace
Writing your first test using jest-environment-vscode-extension
It's almost the same idea as writing any other test using Jest, but we have a powerful API focused on VSCode.
Let's do a walkthrough writing a simple test. We want to test if the "go to the definition" works well at the second x
:
const x = 42
console.log(x)
1 - Firstly, our test doesn't depend on a workspace. Then we'll write it at tests/no-workspace/definitions.test.ts
. Usually, a test depends on a workspace if it interacts with other files on the same workspace.
2 - Let's write the test itself:
// get some things from the global variable `vscode`
const { Position, Range } = vscode
describe('#Definition', () => {
it('on message interpolation', () => {
// create a new file
return using({
files: {
'index.js': dedent(`
const x = 42
console.log(x)
`),
}},
async (mapFileToDoc) => {
// on the file `index.js`, take the definitions at 1:12 (the `x` within the `console.log`)
const definitions = await take.definitions(mapFileToDoc['index.js'], new Position(1, 12))
// assert that it's as the expected
expect(definitions).toHaveLength(1)
expect(definitions[0]).toMatchObject({
originSelectionRange: new Range(new Position(1, 12), new Position(1, 13)),
targetRange: new Range(new Position(0, 12), new Position(0, 0)),
targetSelectionRange: new Range(new Position(0, 6), new Position(0, 7)),
})
})
})
})
On the above test, we used some variables injected by jest-environment-vscode-extension
: vscode
, using
, dedent
, and take
. Think of them as the Jest's describe
or it
, but focused on helping you while working with VSCode.
Let's talk about them!
API
using
Our most useful function.
It creates the files and, optionally, can mock VSCode's functions. It receives a callback and, when it's finished, clear the files and mocks.
Files
You can create as many files as needed, and their TextDocument
is sent to the callback:
using(
{
files: {
'index.js': '"example";',
'foo.js': '1;',
'bar.js': '2;',
},
},
async (mapFileToDoc) => {
mapFileToDoc['index.js'] // TextDocument
mapFileToDoc['foo.js'] // TextDocument
mapFileToDoc['bar.js'] // TextDocument
}
)
Mocks
There are some VSCode features in which we can't manipulate, such as the window.showQuickPick
. But no worries! We can easily mock it:
using(
{
files: {
'index.js': '"example";',
},
mocks: {
'window.showQuickPick': async () => 'My Option',
},
},
async (mapFileToDoc) => {
}
)
Now, if the extension calls window.showQuickPick
it'll return Promise<'My Option'>
.
But there is a rule to use mocks: You should ensure that the extension is initialized. For example, let's say that your extension is initialized only when there is a .ml
file in the workspace:
"activationEvents": [
"workspaceContains:**/*.ml"
]
So you should run the tests using workspace and create at least one .ml
file:
using(
{
files: {
'main.ml': 'let hello () = print_endline "hey there"',
},
mocks: {
'window.showQuickPick': async () => 'My Option',
},
},
async (mapFileToDoc) => {
}
)
dedent
Function to remove indentation. Helpful with using
.
vscode
It's the same vscode
used by the extension itself. So you can use it to manipulate the VSCode.
For example, if you want to open and show a document, you should do:
const { workspace, window } = vscode
const doc = await workspace.openTextDocument(mapFileToDoc['index.js'])
await window.showTextDocument(doc)
It doesn't export the types. If you want them, you should do:
import type { Position } from 'vscode'
const printPosition = (position: Position) => {
console.log({
line: position.line,
character: position.character,
})
}
take
It exposes many helper functions to take values from the VSCode. Just use TypeScript's intellisense to explore what it has.
waitFor
It exposes a helper function to wait for something.
For example, if your extension takes time to initialize, it can be useful:
const waitForDocumentSymbols = async (doc, position) => {
return await waitFor(async () => {
const hovers = await take.hovers(doc, position)
expect(hovers).toHaveLength(1)
return hovers
})
}
describe("#Document Symbol", () => {
it("includes function declaration", () => {
return using(
{
files: {
'main.ml': 'let hello () = print_endline "hey there"',
},
},
async (mapFileToDoc) => {
const symbols = await waitForDocumentSymbols(mapFileToDoc['main.ml'])
expect(symbols[0]).toMatchObject({
name: 'hello',
detail: 'unit -> unit',
})
}
)
})
})