Note: The Listed platform will permanently shut down December 31, 2026. Your data published on Listed will still be available in your personal Standard Notes account. Learn more

How to use vitest on Visual Stduio TestExplorer

The fact that you read this title and opened this page means I'm sure that you know what is esproj.

https://learn.microsoft.com/en-us/visualstudio/javascript/unit-testing-javascript-with-visual-studio?view=vs-2022&tabs=jest

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 :)


You'll only receive email when they publish something new.

More from iwate
All posts