Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for parameterized tests #51

Merged
merged 19 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,34 @@ or add a specific keymap to run tests with watch mode:
vim.api.nvim_set_keymap("n", "<leader>tw", "<cmd>lua require('neotest').run.run({ jestCommand = 'jest --watch ' })<cr>", {})
```

### Parameterized tests

If you want to allow to `neotest-jest` to discover parameterized tests you need to enable flag
`jest_test_discovery` in config setup:
```lua
require('neotest').setup({
...,
adapters = {
require('neotest-jest')({
...,
jest_test_discovery = false,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
jest_test_discovery = false,
jest_test_discovery = true,

Did you mean this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

acutally I meant to use false here, because if someone does not diable discovery key and automatically will update the plugin, this option will run jest commands on all of the files in the project. I think that this solution should be opt-in

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but wouldn't it be misleading if the e.g. for enabling the discovery displayed the value as false?

}),
}
})
```
Its also recommended to disable `neotest` `discovery` option like this:
```lua
require("neotest").setup({
...,
discovery = {
enabled = false,
},
})
```
because `jest_test_discovery` runs `jest` command on file to determine
what tests are inside the file. If `discovery` would be enabled then `neotest-jest`
would spawn a lot of procesees.

### Monorepos
If you have a monorepo setup, you might have to do a little more configuration, especially if
you have different jest configurations per package.
Expand Down
138 changes: 73 additions & 65 deletions lua/neotest-jest/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ local async = require("neotest.async")
local lib = require("neotest.lib")
local logger = require("neotest.logging")
local util = require("neotest-jest.util")
local jest_util = require("neotest-jest.jest-util")
local parameterized_tests = require("neotest-jest.parameterized-tests")

---@class neotest.JestOptions
---@field jestCommand? string|fun(): string
Expand Down Expand Up @@ -47,7 +49,6 @@ local function rootProjectHasJestDependency()
return true
end


---@param path string
---@return boolean
local function hasJestDependency(path)
Expand Down Expand Up @@ -88,6 +89,9 @@ adapter.root = function(path)
return lib.files.match_root_pattern("package.json")(path)
end

local getJestCommand = jest_util.getJestCommand
local getJestConfig = jest_util.getJestConfig

---@param file_path? string
---@return boolean
function adapter.is_test_file(file_path)
Expand Down Expand Up @@ -116,6 +120,35 @@ function adapter.filter_dir(name)
return name ~= "node_modules"
end

local function get_match_type(captured_nodes)
if captured_nodes["test.name"] then
return "test"
end
if captured_nodes["namespace.name"] then
return "namespace"
end
end

-- Enrich `it.each` tests with metadata about TS node position
function adapter.build_position(file_path, source, captured_nodes)
local match_type = get_match_type(captured_nodes)
if not match_type then
return
end

---@type string
local name = vim.treesitter.get_node_text(captured_nodes[match_type .. ".name"], source)
local definition = captured_nodes[match_type .. ".definition"]

return {
type = match_type,
path = file_path,
name = name,
range = { definition:range() },
is_parameterized = captured_nodes["each_property"] and true or false,
}
end

---@async
---@return neotest.Tree | nil
function adapter.discover_positions(path)
Expand Down Expand Up @@ -182,79 +215,45 @@ function adapter.discover_positions(path)
function: (call_expression
function: (member_expression
object: (identifier) @func_name (#any-of? @func_name "it" "test")
property: (property_identifier) @each_property (#eq? @each_property "each")
)
)
arguments: (arguments (string (string_fragment) @test.name) [(arrow_function) (function)])
)) @test.definition
]]

return lib.treesitter.parse_positions(path, query, { nested_tests = true })
end

---@param path string
---@return string
local function getJestCommand(path)
local gitAncestor = util.find_git_ancestor(path)

local function findBinary(p)
local rootPath = util.find_node_modules_ancestor(p)
local jestBinary = util.path.join(rootPath, "node_modules", ".bin", "jest")

if util.path.exists(jestBinary) then
return jestBinary
end

-- If no binary found and the current directory isn't the parent
-- git ancestor, let's traverse up the tree again
if rootPath ~= gitAncestor then
return findBinary(util.path.dirname(rootPath))
end
end

local foundBinary = findBinary(path)

if foundBinary then
return foundBinary
end

return "jest"
end

local jestConfigPattern = util.root_pattern("jest.config.{js,ts}")

---@param path string
---@return string|nil
local function getJestConfig(path)
local rootPath = jestConfigPattern(path)

if not rootPath then
return nil
end
local positions = lib.treesitter.parse_positions(path, query, {
nested_tests = false,
build_position = 'require("neotest-jest").build_position',
})

local jestJs = util.path.join(rootPath, "jest.config.js")
local jestTs = util.path.join(rootPath, "jest.config.ts")
local parameterized_tests_positions =
parameterized_tests.get_parameterized_tests_positions(positions)

if util.path.exists(jestTs) then
return jestTs
if adapter.jest_test_discovery and #parameterized_tests_positions > 0 then
parameterized_tests.enrich_positions_with_parameterized_tests(
positions:data().path,
parameterized_tests_positions
)
end

return jestJs
return positions
end

local function escapeTestPattern(s)
return (
s:gsub("%(", "%\\(")
:gsub("%)", "%\\)")
:gsub("%]", "%\\]")
:gsub("%[", "%\\[")
:gsub("%*", "%\\*")
:gsub("%+", "%\\+")
:gsub("%-", "%\\-")
:gsub("%?", "%\\?")
:gsub("%$", "%\\$")
:gsub("%^", "%\\^")
:gsub("%/", "%\\/")
:gsub("%'", "%\\'")
:gsub("%)", "%\\)")
:gsub("%]", "%\\]")
:gsub("%[", "%\\[")
:gsub("%*", "%\\*")
:gsub("%+", "%\\+")
:gsub("%-", "%\\-")
:gsub("%?", "%\\?")
:gsub("%$", "%\\$")
:gsub("%^", "%\\^")
:gsub("%/", "%\\/")
:gsub("%'", "%\\'")
)
end

Expand Down Expand Up @@ -295,10 +294,10 @@ end

local function cleanAnsi(s)
return s:gsub("\x1b%[%d+;%d+;%d+;%d+;%d+m", "")
:gsub("\x1b%[%d+;%d+;%d+;%d+m", "")
:gsub("\x1b%[%d+;%d+;%d+m", "")
:gsub("\x1b%[%d+;%d+m", "")
:gsub("\x1b%[%d+m", "")
:gsub("\x1b%[%d+;%d+;%d+;%d+m", "")
:gsub("\x1b%[%d+;%d+;%d+m", "")
:gsub("\x1b%[%d+;%d+m", "")
:gsub("\x1b%[%d+m", "")
end

local function findErrorPosition(file, errStr)
Expand Down Expand Up @@ -383,15 +382,19 @@ function adapter.build_spec(args)
-- pos.id in form "path/to/file::Describe text::test text"
local testName = string.sub(pos.id, string.find(pos.id, "::") + 2)
testName, _ = string.gsub(testName, "::", " ")
testNamePattern = "'^" .. escapeTestPattern(testName)
testNamePattern = escapeTestPattern(testName)
testNamePattern = pos.is_parameterized
and parameterized_tests.replaceTestParametersWithRegex(testNamePattern)
or testNamePattern
testNamePattern = "'^" .. testNamePattern
if pos.type == "test" then
testNamePattern = testNamePattern .. "$'"
else
testNamePattern = testNamePattern .. "'"
end
end

local binary = getJestCommand(pos.path)
local binary = args.jestCommand or getJestCommand(pos.path)
local config = getJestConfig(pos.path) or "jest.config.js"
local command = vim.split(binary, "%s+")
if util.path.exists(config) then
Expand Down Expand Up @@ -512,6 +515,11 @@ setmetatable(adapter, {
return opts.strategy_config
end
end

if opts.jest_test_discovery then
adapter.jest_test_discovery = true
end

return adapter
end,
})
Expand Down
78 changes: 78 additions & 0 deletions lua/neotest-jest/jest-util.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
local util = require("neotest-jest.util")

local M = {}

function M.is_callable(obj)
return type(obj) == "function" or (type(obj) == "table" and obj.__call)
end

-- Returns jest binary from `node_modules` if that binary exists and `jest` otherwise.
---@param path string
---@return string
function M.getJestCommand(path)
local gitAncestor = util.find_git_ancestor(path)

local function findBinary(p)
local rootPath = util.find_node_modules_ancestor(p)
local jestBinary = util.path.join(rootPath, "node_modules", ".bin", "jest")

if util.path.exists(jestBinary) then
return jestBinary
end

-- If no binary found and the current directory isn't the parent
-- git ancestor, let's traverse up the tree again
if rootPath ~= gitAncestor then
return findBinary(util.path.dirname(rootPath))
end
end

local foundBinary = findBinary(path)

if foundBinary then
return foundBinary
end

return "jest"
end

local jestConfigPattern = util.root_pattern("jest.config.{js,ts}")

-- Returns jest config file path if it exists.
---@param path string
---@return string|nil
function M.getJestConfig(path)
local rootPath = jestConfigPattern(path)

if not rootPath then
return nil
end

local jestJs = util.path.join(rootPath, "jest.config.js")
local jestTs = util.path.join(rootPath, "jest.config.ts")

if util.path.exists(jestTs) then
return jestTs
end

return jestJs
end

-- Returns neotest test id from jest test result.
-- @param testFile string
-- @param assertionResult table
-- @return string
function M.get_test_full_id_from_test_result(testFile, assertionResult)
local keyid = testFile
local name = assertionResult.title

for _, value in ipairs(assertionResult.ancestorTitles) do
keyid = keyid .. "::" .. value
end

keyid = keyid .. "::" .. name

return keyid
end

return M