Skip to content

religiosa1/neotest-node

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Neotest Adapter for node test runner

Neovim Neotest adapter for node test runner

screenrecording-2025-12-31_14-03-12.mp4

This isn't for running jest or vitest tests, but node built-in test runner tests (but it will play along nicely if you have neotest-vitest or neotest-jest).

Requires node 18+, for typescript support you need node v22.18.0+ (22.6.0 if you add --experimental-strip-types to args)

Features

  • run node test runner tests and suites from your nvim;
  • test results streaming - your results will appear in the UI one by one as they're processed by the node;
  • detection of imports coming from node:test, so the plugin plays along nicely with your existing vitest/jest/bun adapter setup and doesn't trigger on foreign tests;
  • DAP debugger connection;
  • Typescript and JS test suites;
  • environment and args parameters (e.g. for setting up global setup/teardown)
  • output capture for you to see your console.logs and stuff run in a test;

Not supported

  • dynamic/parametrezied tests, e.g. this won't work:
    for (const arg of ["foo", "bar"]) {
      it(`smarty-pants ${arg} test`, (t) => {
        t.assert(false, "this won't work");
      });
    }
    
  • template strings and any kind of string manipulation in test/suite names, tests must be statically analyzable
  • import renames of it/test/describe/suite -- why would you do that?..

Installation

Lazyvim

In your neotest setup (e.g. lua/plugins/neotest.lua)

return {
  {
    "nvim-neotest/neotest",
    dependencies = {
      "religios1/neotest-node"
    },
    opts = {
      -- notice if you also mixed vitest/jest/bun tests in your project, this
      -- adapter most likely must come last, otherwise other adapters will
      -- intercept the test file
      adapters = { "neotest-node" }
    },
  },
}

Configuration options (and their default values)

Default values are provided for the reference, you don't need to copy them.

return {
  {
    "nvim-neotest/neotest",
    dependencies = {
      "religios1/neotest-node"
    },
    opts = {
      adapters = {
        ["neotest-node"] = {
          ---Additional environment options
          ---@type table<string, string> | fun(): table<string, string>
          env = function()
            return {}
          end,
          ---Test command (`node --test`) current working dir
          ---@type string | fun(position_path: string): string?
          cwd = function(position_path)
            local lib = require("neotest.lib")
            return lib.files.match_root_pattern("package.json")(position_path)
          end,
          ---Filtering out dirs from tests detection
          ---@type fun(name: string, rel_path: string, root: string): boolean
          filter_dir = function (name, rel_path, root)
            return name ~= "node_modules"
          end,
          ---Is file with given path a node test runner test file?
          ---@type fun(file_path: string): boolean
          is_test_file = function (file_path)
          	if file_path:match(".*%.test%.[cm]?[tj]sx?$") == nil
              and file_path:match(".*%.spec%.[cm]?[tj]sx?$") == nil then
              return false
            end
            local util = require("neotest-node.util")
            return util.has_node_test_imports(file_path)
          end,
          ---Test command additional arguments
          ---@type string[] | fun(args: neotest.RunArgs): string[]
          args = {},
        }
      },
    },
  },
}

Implementation details

Plugin is running node --test --test-reporter tap for the selected file/ test pattern, and then parses node test runner TAP output to report results and capture potential error messages + error lines.

Node test detection

As I can imagine you also have vitest/jest/bun or whatever else in your adapters, we must differentiate between node tests vs any other solution. jest and vitest adapters check presence of their corresponding testrunner in dependencies, while bun can check for bun lockfile.

We don't have this option, as node test runner won't be present in deps -- it comes out of the box.

So instead of inspecting package.json (which isn't required for this adapter), if the file has the correct extension (e.g. foo.test.ts or bar.spec.js) we're reading the first 2000 chars from the file and trying to find an import from node:test with a regex (be that CJS or ESM import).

We're using regex instead of treesitter, to avoid extra overhead of parsing every test files just to determine if we should do anything with a file.

This detection must happen in is_test_file adapter function -- neotest passes test execution to the first adapter matched by its is_test_file function. Iteration over adapters is performed with pairs() call over object, so order of adapters matter but not guaranteed.

To play it safe, you actually need to add verification function for all of you ts/js adapter, such as notest-jest, neotest-vite, etc.

require("neotest-node.util").has_node_test_imports(file_path)

Example neotest opts:

  {
    "nvim-neotest/neotest",
    -- dependencies and other blocks goes here, omitted for brevity
    opts = {
        ["neotest-jest"] = {
          isTestFile = function(file_path)
            if not file_path then
              return false
            end
            if require("neotest-node/util").has_node_test_imports(file_path) then
              return false
            end
            return vim.fn.fnamemodify(file_path, ":e:e"):match("test%.[jt]sx?$") ~= nil
          end,
        },
        "neotest-node",
    }
  }

This introduces some overhead, which I consider negliable for my use-cases, but if you want to disable this functionality completely (in cases you don't have any other js/ts adapters besides this one) you can pass your custom is_test_file in the adapter options in your config, e.g.:

-- opts in config:
{
  is_test_file = function (file_path)
    return file_path:match(".*%.test%.[cm]?[tj]sx?$") ~= nil
  end
}

Local Development

Running Tests

For running tests you need a neovim setup and node 20+ available in your path.

To run the whole test suite:

./scripts/test.sh

You can launch a specific unit-test by:

./scripts/test.sh tests/yaml-diagnostics-parser_spec.lua

On the initial launch script will retrieve its deps by cloning the corresponding github repos into .testsdep folder.

License

neotest-node is MIT licensed.

About

neotest adapter for node test runner

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors