How to use vitest on Visual Stduio TestExplorer
June 21, 2024•764 words
The fact that you read this title and opened this page means I'm sure that you know what is esproj.
esproj
is supporting only the following test frameworks.
- Mocha (mochajs.org)
- Jasmine (Jasmine.github.io)
- Tape (github.com/substack/tape)
- Jest (jestjs.io)
But, In 2024, vitest
is one of the most popular frameworks for testing.
I want to use it.
1. Install framework script
Create a js file at <VSInstallDir>\Common7\IDE\Extensions\Microsoft\JavaScript\TestFrameworks\Vitest\vitest.js
In my case, <VSInstallDir>
is C:\Program Files\Microsoft Visual Studio\2022\Enterprise
.
If <VSInstallDir>Common7\IDE\Extensions\Microsoft\JavaScript\TestFrameworks
doesn't exist, you must install Visual Studio NodeJS Developer Tools
in first.
And copy the next code to the js file you create.
// @ts-check
"use strict";
const fs = require('fs');
const os = require('os');
const path = require('path');
function normalize(file) {
file = file.replaceAll('\\', '/')
if (fs.existsSync(file)) {
return file
}
const ext = path.extname(file)
const name = file.slice(0, file.length - ext.length)
const ts = name + '.ts'
if (fs.existsSync(ts)) {
return ts
}
const tsx = name + '.tsx'
if (fs.existsSync(tsx)) {
return tsx
}
const jsx = name + '.jsx'
if (fs.existsSync(jsx)) {
return jsx
}
return file
}
class VSTestsCollector {
constructor(callback) {
this.callback = callback
this.tests = []
}
init(ctx) {
this.ctx = ctx
}
onFinished(files, errors, coverage) {
this.ctx.close()
this.callback?.(this.tests)
}
onCollected(files) {
const visit = (items, prefix = null) => {
for (let item of items) {
if (item.type === 'test') {
this.tests.push({
filepath: item.file.filepath,
line: item.location.line - 1,
column: item.location.column,
suite: prefix,
name: item.name
})
}
if (item.type === 'suite') {
visit(item.tasks, `${prefix ?? ''} ${item.name}`.trimStart())
}
}
}
visit(files)
}
}
function collectTests(projectFolder, files) {
return new Promise(function (resolve) {
(async function () {
const vitestModule = await detectPackage(projectFolder, 'vitest', 'dist/node.js');
if (!vitestModule) {
console.log('vitest is not found');
return;
}
const reporter = new VSTestsCollector(resolve);
const ctx = await vitestModule.createVitest('test', {
dir: projectFolder,
config: null,
workspace: null,
watch: true,
api: false,
reporter: undefined,
reporters: [reporter],
ui: false,
includeTaskLocation: true,
}, {
middlewareMode: true,
})
reporter.init(ctx)
await ctx.getTestFilepaths() // must
ctx.runFiles(files.flatMap(file => ctx.getProjectsByTestFile(normalize(file))), false)
}())
})
}
const find_tests = function (testFileList, discoverResultFile, projectFolder) {
return collectTests(projectFolder, testFileList.split(';')).then(tests => {
const fd = fs.openSync(discoverResultFile, 'w')
fs.writeSync(fd, JSON.stringify(tests))
fs.closeSync(fd)
})
};
class VSResultsReporter {
constructor(callback) {
this.callback = callback
this.tests = []
}
init(ctx) {
this.ctx = ctx
}
onFinished(files, errors, coverage) {
const visit = (items, prefix = '') => {
for (let item of items) {
const name = `${prefix} ${item.name}`.trimStart()
if (item.type === 'test') {
this.tests.push({
type: item.result.state === 'skip' ? 'pending' : 'result',
fullyQualifiedName: name,
result: {
passed: item.result.state === 'pass',
pending: item.result.state === 'skip',
stderr: item.result.errors?.map(err => err.message).join('\n') ?? '',
stdout: '',
fullyQualifiedName: name
}
})
}
if (item.type === 'suite') {
visit(item.tasks, name)
}
}
}
visit(files)
this.ctx.close()
this.callback?.(this.tests)
}
}
async function runTests(projectFolder, files) {
return new Promise(function (resolve) {
(async function () {
const vitestModule = await detectPackage(projectFolder, 'vitest', 'dist/node.js');
if (!vitestModule) {
console.log('vitest is not found');
return;
}
const reporter = new VSResultsReporter(resolve);
const ctx = await vitestModule.createVitest('test', {
dir: projectFolder,
config: null,
workspace: null,
watch: true,
api: false,
reporter: undefined,
reporters: [reporter],
ui: false,
includeTaskLocation: true,
}, {
middlewareMode: true,
})
reporter.init(ctx)
await ctx.getTestFilepaths() // must
ctx.runFiles(files.flatMap(file => ctx.getProjectsByTestFile(normalize(file))), false)
}())
})
}
const run_tests = function (context) {
const projectFolder = context.testCases[0].projectFolder;
// NODE_ENV sets the environment context for Node, it can be development, production or test and it needs to be set up for jest to work.
// If no value was assigned, assign test.
process.env.NODE_ENV = process.env.NODE_ENV || 'test';
return (async function () {
// Start all test cases, as jest is unable to filter out independently
for (const testCase of context.testCases) {
context.post({
type: 'test start',
fullyQualifiedName: testCase.fullyQualifiedName
});
}
try {
const results = await runTests(projectFolder, context.testCases.map(testCase => testCase.testFile))
for (let item of results) {
item.fullyQualifiedName = context.getFullyQualifiedName(item.fullyQualifiedName)
item.result.fullyQualifiedName = item.fullyQualifiedName
context.post(item);
}
} catch (error) {
logError(error);
}
context.post({
type: 'end'
});
}())
};
async function detectPackage(projectFolder, packageName, file) {
try {
const packagePath = path.join(projectFolder, 'node_modules', packageName, file);
const pkg = await import(`file:///${packagePath}`);
return pkg;
} catch (ex) {
console.log(ex)
logError(
`Failed to find "${packageName}" package. "${packageName}" must be installed in the project locally.` + os.EOL +
`Install "${packageName}" locally using the npm manager via solution explorer` + os.EOL +
`or with ".npm install ${packageName} --save-dev" via the Node.js interactive window.`);
return null;
}
}
function logError() {
var errorArgs = Array.from(arguments);
errorArgs.unshift("NTVS_ERROR:");
console.error.apply(console, errorArgs);
}
module.exports.find_tests = find_tests;
module.exports.run_tests = run_tests;
2. Set JavaScriptTestFramework Vitest
<Project Sdk="Microsoft.VisualStudio.JavaScript.Sdk/1.0.1147595">
<PropertyGroup>
...
<JavaScriptTestFramework>Vitest</JavaScriptTestFramework>
...
</PropertyGroup>
</Project>
3. Run all tests on TestExplorer
Have fun :)