Compatible Runners

Not tied to any runner

Linters

N/A

Commands
  • test: Vitest integrates itself with the crafty test command
  • ide: Vitest integrates itself with the crafty ide command
Related presets

Crafty provides a preset that will run Vitest once crafty test is executed.

It adds safe defaults to be able to run your tests with your configuration, owns the generated Vitest config used by Crafty and your IDE, and provides an extension hook that allows you and other presets to extend its configuration.

Vitest is a Node-based runner. Unless you set another environment, the tests run in a Node environment and not in a real browser.

If you need another environment, you can override it in the vitest(...) hook.

We recommend that you use a separate tool for browser end-to-end tests if you need them.

File name Conventions

Vitest will look for test files with any of the following popular naming conventions:

  • Files with supported extensions in __tests__ folders.
  • Files with .test.<extension> suffix.
  • Files with .spec.<extension> suffix.

By default, Crafty supports js, mjs, and cjs test files. json stays resolvable, but does not take part in test discovery.

The .test.<extension> / .spec.<extension> files (or the __tests__ folders) can be located at any depth under the project root.

We recommend to put the test files (or __tests__ folders) next to the code they are testing so that relative imports appear shorter. For example, if App.test.js and App.js are in the same folder, the test needs to import App from "./App" instead of a long relative path.

Using crafty-preset-babel or crafty-preset-swc will add jsx as a supported test file extension. Using crafty-preset-typescript will add the TypeScript extensions ts, tsx, mts, and cts.

crafty test

Running crafty test will run all tests and exit. But you can use any option provided by Vitest itself.

For example, crafty test --watch will run your tests in watch mode. This mode will run all your tests once and then wait for code or test changes to rerun the concerned tests.

Crafty supports one active test runner. If both Jest and Vitest are configured, crafty test fails with a clear error.

Crafty-specific CLI options

The Vitest preset also provides extra options:

  • --moduleDirectories <dir[,dir...]>: adds directories to module lookup beyond node_modules.
  • --moduleFileExtensions <ext[,ext...]>: adds file extensions to module resolution and test discovery.
  • --reporters <name[,name...]>: configures Vitest reporters. The special values sonar and vitest-sonar-reporter are normalized to Crafty’s Sonar reporter defaults. Crafty resolves the reporter from the preset, writes coverage/sonar-report.xml by default, and keeps relative file paths unless you set reportedFilePath: "absolute".

You can pass a comma-separated list or repeat the option.

Writing Tests

To create tests, add it() (or test()) blocks with the name of the test and its code. You may optionally wrap them in describe() blocks for logical grouping but this is neither required nor recommended.

Crafty enables Vitest’s test() and expect() globals for you. A basic test could look like this:

import sum from "./sum";

test("sums numbers", () => {
  expect(sum(1, 2)).toEqual(3);
  expect(sum(2, 2)).toEqual(4);
});

All expect() matchers supported by Vitest are documented on the official website.

Focusing and Excluding Tests

Use test.skip() or it.skip() to temporarily exclude a test from being executed. Vitest also provides focused test helpers and CLI filters when you want to run a single test in isolation.

Coverage Reporting

crafty-preset-vitest ships with V8 coverage support out of the box.

Tests run slower with coverage enabled, so we recommend running it separately from your normal watch workflow.

Run crafty test --coverage to include a coverage report. Crafty keeps Vitest coverage output under coverage/ and includes coverage/lcov.info by default so SonarQube and similar tools can ingest it. Unless you override them, Crafty normalizes coverage to use provider v8 and reporter lcov.

If you need to customize Crafty’s coverage settings, assign options.test.coverage through the vitest(crafty, options, context) hook.

module.exports = {
  vitest(crafty, options) {
    options.test.coverage = {
      reportsDirectory: "./reports/coverage",
      reporter: ["lcov"],
    };
  },
};

Snapshot Testing

Vitest supports snapshot testing for values, rendered components, and more. Read more about snapshot testing.

Vitest configuration ownership

Crafty owns the Vitest configuration it runs.

  • crafty test computes the Vitest config from Crafty state and ignores official vitest.config.* files.
  • crafty ide writes the IDE-facing vitest.config.mjs file and removes alternative filenames such as vitest.config.js, vitest.config.cjs, vitest.config.mts, vitest.config.ts, and vitest.config.cts.

If you need to change Vitest behavior, use the vitest(crafty, options, context) hook rather than maintaining a separate vitest.config.* file.

Extending the configuration

Each preset and crafty.config.js can define the vitest(crafty, options, context) function to override Vitest’s configuration.

const path = require("node:path");
const MODULES = path.join(__dirname, "..", "node_modules");

module.exports = {
  /**
   * Represents the extension point for Vitest configuration
   * @param {Crafty} crafty - The instance of Crafty.
   * @param {Object} options - The Vitest configuration object
   * @param {Object} context - Crafty helpers for resolution and runtime plugins
   */
  vitest(crafty, options, context) {
    context.moduleDirectories.push(MODULES);
    context.moduleFileExtensions.push("ts");

    options.test.setupFiles = options.test.setupFiles || [];
    options.test.setupFiles.push(require.resolve("./testSetup"));
    options.test.environment = "jsdom";
  },
};

Because Crafty computes the configuration before starting Vitest, options must stay serializable.

Reporter callbacks such as vitest-sonar-reporter’s onWritePath option are not supported through this hook. For Sonar path control, use reportedFilePath: "absolute" or reportedFilePath: "relative" on the Sonar reporter config.

Use context.moduleDirectories and context.moduleFileExtensions to extend module resolution and test discovery.

Migrating an existing vitest.config.*

When you migrate from a raw Vitest configuration, move repo-specific Vitest settings into the vitest(crafty, options, context) hook.

Common fields that still need to be re-expressed manually are:

  • resolve.alias
  • test.include
  • test.exclude
  • test.testTimeout
  • test.hookTimeout
  • test.pool
  • test.reporters

Crafty always adds its own default discovery globs for supported extensions. If your repository contains fixtures, sample apps, or embedded workspaces, use options.test.exclude to narrow discovery as needed. A common pattern is to exclude paths such as fixtures/**, examples/**, or samples/**.

If you replace options.test.reporters, you own the full reporter list for that package. Add sonar explicitly when you still want the default Sonar XML report alongside a custom reporter such as verbose.

The full list of available configuration options is available on the official website.

Runtime Vite plugins

If you need a Vite plugin at runtime, register it through context.runtimePlugins instead of attaching a plugin object directly to options.plugins.

module.exports = {
  vitest(crafty, options, context) {
    context.runtimePlugins.push({
      pluginPath: require.resolve("./vitest-plugin"),
      options: {
        mode: "test",
      },
    });
  },
};

The module at pluginPath must export a function that receives options and returns a Vite plugin. If you attach non-serializable values directly to options, Crafty rejects them with a clear error.

crafty ide

Running crafty ide generates a package-local vitest.config.mjs file for IDE discovery.

In a monorepo, run crafty ide from each package directory that has its own crafty.config.js and uses @swissquote/crafty-preset-vitest.

Crafty does not modify .vscode/settings.json for Vitest. The generated vitest.config.mjs is intended to be discovered by the official Vitest IDE integration.

Read more about Vitest IDE integration.

SonarQube Integration

Most of the time, at Swissquote, we use SonarQube to check our code quality. More often than not, we add a reporter to create a SonarQube test report.

crafty-preset-vitest comes out of the box with a sonar report that is written to coverage/sonar-report.xml.

Consumers do not need to add vitest-sonar-reporter directly unless they import it themselves. Crafty resolves the reporter from the preset.

This report is automatically added when no reporters are configured after the Crafty hooks have run. The sonar alias, and the raw vitest-sonar-reporter name, are normalized to that default output path with relative file paths by default. This keeps the XML portable across local runs and multi-platform CI.

When coverage is enabled through crafty test --coverage, Crafty also keeps Vitest coverage artifacts under coverage/ and includes coverage/lcov.info by default.

If your Sonar setup requires absolute file paths, set reportedFilePath: "absolute" on the Sonar reporter config.

You can decide to change this configuration by overriding options.test.reporters

module.exports = {
  /**
   * Represents the extension point for Vitest configuration
   * @param {Crafty} crafty - The instance of Crafty.
   * @param {Object} options - The Vitest configuration object
   */
  vitest(crafty, options) {
    options.test.reporters = [
      "verbose",
      ["sonar", { reportedFilePath: "absolute" }],
    ];
  },
};