create runner with ChatGPT-Codex by using the AGENTS.md
Some checks failed
tests / test (push) Failing after 4s
Some checks failed
tests / test (push) Failing after 4s
This commit is contained in:
20
.gitea/workflows/tests.yaml
Normal file
20
.gitea/workflows/tests.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
name: tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install Neovim (AppImage)
|
||||||
|
run: |
|
||||||
|
curl -L -o nvim.appimage https://github.com/neovim/neovim/releases/download/v0.11.4/nvim.appimage
|
||||||
|
chmod +x nvim.appimage
|
||||||
|
sudo mv nvim.appimage /usr/local/bin/nvim
|
||||||
|
- name: Run tests
|
||||||
|
run: bash run_test.sh
|
||||||
239
AGENTS.md
Normal file
239
AGENTS.md
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
# runner-agents.md — test-samurai Runner-API
|
||||||
|
|
||||||
|
Ziel: Diese Datei beschreibt die öffentliche Runner-API, die ein neuer Runner
|
||||||
|
implementieren muss, damit alle Commands vollständig unterstützt werden.
|
||||||
|
|
||||||
|
## Modulform (Pflicht)
|
||||||
|
|
||||||
|
- Der Runner ist ein Lua-Modul, das eine Table mit Funktionen zurückgibt.
|
||||||
|
- Beispiel:
|
||||||
|
- `local runner = {}`
|
||||||
|
- `return runner`
|
||||||
|
- Der Modulpfad in `runner_modules` muss exakt zum Dateipfad unter `lua/` passen.
|
||||||
|
- Beispiel: `lua/test-samurai-go-runner/init.lua` -> `require("test-samurai-go-runner")`.
|
||||||
|
- `lua/init.lua` wäre `require("init")` und ist kollisionsanfällig; vermeiden.
|
||||||
|
|
||||||
|
## Pflichtfunktionen (volle Command- und Keymap-Unterstützung)
|
||||||
|
|
||||||
|
- `is_test_file(bufnr) -> boolean`
|
||||||
|
- Wird für die Runner-Auswahl genutzt.
|
||||||
|
- `find_nearest(bufnr, row, col) -> spec|nil, err?`
|
||||||
|
- Für `TSamNearest`.
|
||||||
|
- `spec.file` muss gesetzt sein.
|
||||||
|
- Bei Fehler/kein Treffer: `nil, "reason"` zurückgeben.
|
||||||
|
- `build_command(spec) -> command_spec`
|
||||||
|
- Für `TSamNearest`.
|
||||||
|
- `build_file_command(bufnr) -> command_spec`
|
||||||
|
- Für `TSamFile`.
|
||||||
|
- `build_all_command(bufnr) -> command_spec`
|
||||||
|
- Für `TSamAll`.
|
||||||
|
- `build_failed_command(last_command, failures, scope_kind) -> command_spec`
|
||||||
|
- Für `TSamFailedOnly`.
|
||||||
|
- `parse_results(output) -> results`
|
||||||
|
- Fallback-Parser für vollständigen Output.
|
||||||
|
- `output_parser() -> { on_line, on_complete }`
|
||||||
|
- Muss Streaming unterstützen (Listing wird live befüllt).
|
||||||
|
- `parse_test_output(output) -> table`
|
||||||
|
- Detail-Ausgabe pro Test (Detail-Float und `<cr>` im Listing).
|
||||||
|
- `collect_failed_locations(failures, command, scope_kind) -> items`
|
||||||
|
- Quickfix-Unterstützung (Keymap `<leader>qn`).
|
||||||
|
|
||||||
|
## Output-Parsing (Listing + Summary)
|
||||||
|
|
||||||
|
Beide Varianten müssen vorhanden sein:
|
||||||
|
|
||||||
|
- `parse_results(output) -> results`
|
||||||
|
- `results` muss enthalten:
|
||||||
|
- `passes` (Array von Namen)
|
||||||
|
- `failures` (Array von Namen)
|
||||||
|
- `skips` (Array von Namen)
|
||||||
|
- Optional:
|
||||||
|
- `display = { passes = {}, failures = {}, skips = {} }`
|
||||||
|
- `failures_all` (für Streaming-Parser, um alle bisherigen Failures zu liefern)
|
||||||
|
- Wenn `display` fehlt, werden `passes/failures/skips` direkt im Listing angezeigt.
|
||||||
|
- oder `output_parser() -> { on_line, on_complete }`
|
||||||
|
- `on_line(line, state)` kann `results` liefern (siehe oben).
|
||||||
|
- `on_complete(output, state)` kann `results` liefern (siehe oben).
|
||||||
|
- `on_line` muss Ergebnisse liefern, damit das Listing-Float immer gestreamt wird.
|
||||||
|
|
||||||
|
Hinweis: `parse_results` darf intern `output_parser().on_complete` nutzen, aber beide Funktionen müssen existieren.
|
||||||
|
|
||||||
|
## Listing-Gruppierung fuer Parent/Subtests
|
||||||
|
|
||||||
|
- Wenn Testnamen das Format `Parent/Subtest` oder verschachtelt `Parent/Sub/Subtest` haben
|
||||||
|
und der Parent ebenfalls in den Ergebnissen vorhanden ist, gruppiert das Listing:
|
||||||
|
- Parent kommt vor seinen direkten Kindern.
|
||||||
|
- Mehrstufige Subtests werden hierarchisch gruppiert (Parent -> Kind -> Enkel).
|
||||||
|
- Die Reihenfolge der Kinder folgt der Eingangsreihenfolge des Runners.
|
||||||
|
- Ohne Parent-Eintrag bleibt die normale Reihenfolge erhalten.
|
||||||
|
|
||||||
|
## Detail-Output (TSamShowOutput / <cr> im Listing)
|
||||||
|
|
||||||
|
- `parse_test_output(output) -> table`
|
||||||
|
- Rückgabeform:
|
||||||
|
- `{ [test_name] = { "line1", "line2", ... } }`
|
||||||
|
- `test_name` muss mit `results.*` korrespondieren (gleiches Namensschema).
|
||||||
|
|
||||||
|
## Quickfix-Unterstützung (Failures)
|
||||||
|
|
||||||
|
- `collect_failed_locations(failures, command, scope_kind) -> items`
|
||||||
|
- `items`: Array von Quickfix-Items
|
||||||
|
- `{ filename = "...", lnum = <number>, col = <number>, text = "..." }`
|
||||||
|
|
||||||
|
## Erwartete Datenformen
|
||||||
|
|
||||||
|
- `command_spec`:
|
||||||
|
- `{ cmd = { "binary", "arg1", ... }, cwd = "..." }`
|
||||||
|
- `cmd` darf nicht leer sein.
|
||||||
|
- `cwd` ist optional; wenn nicht gesetzt, nutzt der Core das aktuelle CWD.
|
||||||
|
- `spec` (von `find_nearest`):
|
||||||
|
- Muss mindestens `file` enthalten, z. B.:
|
||||||
|
- `{ file = "...", cwd = "...", test_name = "...", full_name = "...", kind = "..." }`
|
||||||
|
- `results` (für Listing-Float + Keymaps):
|
||||||
|
- `{ passes = { "Name1", ... }, failures = { "Name2", ... }, skips = { "Name3", ... } }`
|
||||||
|
- Optional: `display = { passes = { "DisplayName1", ... }, failures = { "DisplayName2", ... }, skips = { "DisplayName3", ... } }`
|
||||||
|
- `failures` steuert `[ FAIL ]`-Zeilen im Listing und wird von `<leader>nf`/`<leader>pf` genutzt.
|
||||||
|
- `items` (für Quickfix):
|
||||||
|
- `{ { filename = "...", lnum = 1, col = 1, text = "..." }, ... }`
|
||||||
|
- Wird von `<leader>qn` verwendet.
|
||||||
|
|
||||||
|
## Keymaps (Datenlieferung)
|
||||||
|
|
||||||
|
- `<leader>nf` / `<leader>pf`
|
||||||
|
- benötigt `[ FAIL ]`-Einträge im Listing.
|
||||||
|
- Runner muss `results.failures` (und optional `display.failures`) liefern.
|
||||||
|
- `<leader>qn`
|
||||||
|
- springt in die Quickfix-Liste.
|
||||||
|
- Runner muss `collect_failed_locations` implementieren und gültige `items` liefern.
|
||||||
|
- `<cr>` im Listing
|
||||||
|
- öffnet Detail-Float.
|
||||||
|
- Runner muss `parse_test_output` liefern und Testnamen konsistent zu `results.*` halten.
|
||||||
|
|
||||||
|
## Optional empfohlene Metadaten
|
||||||
|
|
||||||
|
- `name` (String)
|
||||||
|
- Wird in Fehlermeldungen und Logs angezeigt.
|
||||||
|
- `framework` (String)
|
||||||
|
- Wird zur Framework-Auswahl (z. B. JS) genutzt.
|
||||||
|
|
||||||
|
## Prompt-Beispiel
|
||||||
|
|
||||||
|
"Erstelle mir anhand der `runner-agents.md` einen neuen Runner für Rust."
|
||||||
|
|
||||||
|
## Minimaler Runner-Skeleton (Template)
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local runner = {
|
||||||
|
name = "my-runner",
|
||||||
|
framework = "my-framework",
|
||||||
|
}
|
||||||
|
|
||||||
|
function runner.is_test_file(bufnr)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.find_nearest(bufnr, row, col)
|
||||||
|
return nil, "no test call found"
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.build_command(spec)
|
||||||
|
return { cmd = { "echo", "not-implemented" }, cwd = spec.cwd }
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.build_file_command(bufnr)
|
||||||
|
return { cmd = { "echo", "not-implemented" } }
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.build_all_command(bufnr)
|
||||||
|
return { cmd = { "echo", "not-implemented" } }
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.build_failed_command(last_command, failures, scope_kind)
|
||||||
|
return { cmd = { "echo", "not-implemented" }, cwd = last_command and last_command.cwd or nil }
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.parse_results(output)
|
||||||
|
return { passes = {}, failures = {}, skips = {}, display = { passes = {}, failures = {}, skips = {} } }
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.output_parser()
|
||||||
|
return {
|
||||||
|
on_line = function(_line, _state)
|
||||||
|
return nil
|
||||||
|
end,
|
||||||
|
on_complete = function(_output, _state)
|
||||||
|
return runner.parse_results(_output)
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.parse_test_output(output)
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.collect_failed_locations(failures, command, scope_kind)
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
|
return runner
|
||||||
|
```
|
||||||
|
|
||||||
|
## Checkliste für neue Runner
|
||||||
|
|
||||||
|
- is_test_file implementiert
|
||||||
|
- find_nearest implementiert (setzt `spec.file`)
|
||||||
|
- build_command implementiert
|
||||||
|
- build_file_command implementiert
|
||||||
|
- build_all_command implementiert
|
||||||
|
- build_failed_command implementiert
|
||||||
|
- parse_results implementiert
|
||||||
|
- output_parser implementiert (Streaming)
|
||||||
|
- parse_test_output implementiert
|
||||||
|
- collect_failed_locations implementiert
|
||||||
|
- command_spec `{ cmd, cwd }` korrekt zurückgegeben
|
||||||
|
|
||||||
|
## Projekt- und Prozessanforderungen
|
||||||
|
|
||||||
|
- Rolle: **TDD-first Entwickler**.
|
||||||
|
- Jede neue Funktion, jedes neue Kommando und jede Verhaltensänderung muss durch Tests abgesichert sein.
|
||||||
|
- Nach jeder Code-Änderung Tests via `bash run_test.sh` ausführen und bei Fehlern korrigieren, bis alles grün ist.
|
||||||
|
- **Nicht raten**:
|
||||||
|
- Bei unklaren oder mehrdeutigen Anforderungen Arbeit stoppen und Klarstellung verlangen.
|
||||||
|
- TODO/NOTE im Code ist zulässig, stilles Raten nicht.
|
||||||
|
- **Keine stillen Änderungen**:
|
||||||
|
- Bestehende Features dürfen nicht unbemerkt geändert oder ersetzt werden.
|
||||||
|
- Notwendige Anpassungen zur Koexistenz mehrerer Features müssen klar erkennbar sein.
|
||||||
|
- Antworten immer auf Deutsch.
|
||||||
|
- Eine englischsprachige `README.md` ist zu erstellen und wird bei Änderungen automatisch aktualisiert.
|
||||||
|
- TDD-Vorgaben (aus `AGENTS.md`) uebernehmen:
|
||||||
|
- Neue Funktionen/Commands/Verhaltensaenderungen muessen getestet werden.
|
||||||
|
- Tests nach jeder Code-Aenderung ausfuehren.
|
||||||
|
- Im Runner erstellter Quellcode ist ebenfalls zu testen.
|
||||||
|
- Eine eigene `run_test.sh` wird im Runner-Repo angelegt.
|
||||||
|
- Eine Gitea-Action ist zu erstellen, die bei jedem Push die Tests ausfuehrt.
|
||||||
|
- Neovim wird per AppImage installiert (kein `apt`).
|
||||||
|
- Runner laeuft auf `gitea-act-runner` mit Raspberry Pi 5 (ARM).
|
||||||
|
- Beispiel (anpassbarer Workflow):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install Neovim (AppImage)
|
||||||
|
run: |
|
||||||
|
curl -L -o nvim.appimage https://github.com/neovim/neovim/releases/download/v0.11.4/nvim.appimage
|
||||||
|
chmod +x nvim.appimage
|
||||||
|
sudo mv nvim.appimage /usr/local/bin/nvim
|
||||||
|
- name: Run tests
|
||||||
|
run: bash run_test.sh
|
||||||
|
```
|
||||||
61
README.md
Normal file
61
README.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# test-samurai-jest-runner.nvim
|
||||||
|
|
||||||
|
Jest.js runner for `test-samurai.nvim`.
|
||||||
|
|
||||||
|
Main plugin: https://gitea.mschirmer.com/m13r/test-samurai.nvim
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Detects Jest test files (`*.test.*`, `*.spec.*`).
|
||||||
|
- Finds nearest `test`/`it` within `describe` blocks.
|
||||||
|
- Builds `npx jest` commands for nearest, file, all, and failed-only runs.
|
||||||
|
- Streams results via a custom Jest reporter using `onTestCaseResult`.
|
||||||
|
- Uses `--testLocationInResults` for Quickfix and `<leader>o` support.
|
||||||
|
|
||||||
|
## Installation (lazy.nvim)
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{
|
||||||
|
"m13r/test-samurai.nvim",
|
||||||
|
dependencies = {
|
||||||
|
"m13r/test-samurai-jest-runner.nvim",
|
||||||
|
},
|
||||||
|
config = function()
|
||||||
|
require("test-samurai").setup({
|
||||||
|
runners = {
|
||||||
|
require("test-samurai-jest-runner"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local Development (lazy.nvim)
|
||||||
|
|
||||||
|
```lua
|
||||||
|
{
|
||||||
|
"m13r/test-samurai.nvim",
|
||||||
|
dependencies = {
|
||||||
|
{
|
||||||
|
"test-samurai-jest-runner.nvim",
|
||||||
|
dir = "/absolute/path/to/test-samurai-jest-runner.nvim",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
config = function()
|
||||||
|
require("test-samurai").setup({
|
||||||
|
runners = {
|
||||||
|
require("test-samurai-jest-runner"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Use the standard `test-samurai.nvim` commands (e.g. `TSamNearest`, `TSamFile`, `TSamAll`, `TSamFailedOnly`).
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The reporter lives at `reporter/test_samurai_jest_reporter.js` and is loaded via `--reporters`.
|
||||||
|
- Test names are reported as `Describe/It` for grouping in the listing.
|
||||||
660
lua/test-samurai-jest-runner/init.lua
Normal file
660
lua/test-samurai-jest-runner/init.lua
Normal file
@@ -0,0 +1,660 @@
|
|||||||
|
local runner = {
|
||||||
|
name = "jest",
|
||||||
|
framework = "javascript",
|
||||||
|
}
|
||||||
|
|
||||||
|
local RESULT_PREFIX = "TSAMURAI_RESULT "
|
||||||
|
local STATUS_MAP = {
|
||||||
|
passed = "passes",
|
||||||
|
failed = "failures",
|
||||||
|
skipped = "skips",
|
||||||
|
pending = "skips",
|
||||||
|
todo = "skips",
|
||||||
|
}
|
||||||
|
|
||||||
|
runner._last_locations = {}
|
||||||
|
runner._last_jest_names = {}
|
||||||
|
|
||||||
|
local function get_buf_path(bufnr)
|
||||||
|
return vim.api.nvim_buf_get_name(bufnr)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_buf_lines(bufnr)
|
||||||
|
return vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function find_root(path, markers)
|
||||||
|
if not path or path == "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local dir = vim.fs.dirname(path)
|
||||||
|
if not dir or dir == "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local found = vim.fs.find(markers, { path = dir, upward = true })
|
||||||
|
if not found or not found[1] then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return vim.fs.dirname(found[1])
|
||||||
|
end
|
||||||
|
|
||||||
|
local function count_char(line, ch)
|
||||||
|
local count = 0
|
||||||
|
for i = 1, #line do
|
||||||
|
if line:sub(i, i) == ch then
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return count
|
||||||
|
end
|
||||||
|
|
||||||
|
local function find_block_end(lines, start_idx)
|
||||||
|
local depth = 0
|
||||||
|
local started = false
|
||||||
|
for i = start_idx, #lines do
|
||||||
|
local line = lines[i]
|
||||||
|
for j = 1, #line do
|
||||||
|
local ch = line:sub(j, j)
|
||||||
|
if ch == "{" then
|
||||||
|
depth = depth + 1
|
||||||
|
started = true
|
||||||
|
elseif ch == "}" then
|
||||||
|
if started then
|
||||||
|
depth = depth - 1
|
||||||
|
if depth == 0 then
|
||||||
|
return i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return start_idx
|
||||||
|
end
|
||||||
|
|
||||||
|
local function match_call_name(lines, idx, keywords)
|
||||||
|
local line = lines[idx] or ""
|
||||||
|
for _, key in ipairs(keywords) do
|
||||||
|
local pattern = "%f[%w_]" .. key .. "[%w_%.]*%s*%(%s*['\"]([^'\"]+)['\"]"
|
||||||
|
local name = line:match(pattern)
|
||||||
|
if name and name ~= "" then
|
||||||
|
return name
|
||||||
|
end
|
||||||
|
local has_call = line:match("%f[%w_]" .. key .. "[%w_%.]*%s*%(")
|
||||||
|
if has_call then
|
||||||
|
local max_idx = math.min(#lines, idx + 3)
|
||||||
|
for j = idx + 1, max_idx do
|
||||||
|
local next_line = lines[j] or ""
|
||||||
|
local next_name = next_line:match("['\"]([^'\"]+)['\"]")
|
||||||
|
if next_name and next_name ~= "" then
|
||||||
|
return next_name
|
||||||
|
end
|
||||||
|
if next_line:find("%)") then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function escape_regex(text)
|
||||||
|
text = text or ""
|
||||||
|
return (text:gsub("([\\.^$|()%%[%]{}*+?%-])", "\\\\%1"))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function build_jest_pattern(parts)
|
||||||
|
local escaped = {}
|
||||||
|
for _, part in ipairs(parts) do
|
||||||
|
table.insert(escaped, escape_regex(part))
|
||||||
|
end
|
||||||
|
return "^.*" .. escape_regex(parts[#parts] or "") .. "$"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function build_jest_prefix_pattern(parts)
|
||||||
|
local tokens = {}
|
||||||
|
for _, part in ipairs(parts or {}) do
|
||||||
|
for token in part:gmatch("%w+") do
|
||||||
|
table.insert(tokens, token)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #tokens == 0 then
|
||||||
|
return escape_regex(parts[#parts] or "") .. ".*"
|
||||||
|
end
|
||||||
|
return table.concat(tokens, ".*") .. ".*"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function to_jest_full_name(name)
|
||||||
|
if not name or name == "" then
|
||||||
|
return name
|
||||||
|
end
|
||||||
|
if not name:find("/", 1, true) then
|
||||||
|
return name
|
||||||
|
end
|
||||||
|
local parts = vim.split(name, "/", { plain = true, trimempty = true })
|
||||||
|
return table.concat(parts, " ")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function find_tests(lines)
|
||||||
|
local tests = {}
|
||||||
|
local describes = {}
|
||||||
|
for i, _line in ipairs(lines) do
|
||||||
|
local describe_name = match_call_name(lines, i, { "describe", "context" })
|
||||||
|
if describe_name then
|
||||||
|
local start_idx = i
|
||||||
|
local end_idx = find_block_end(lines, start_idx)
|
||||||
|
table.insert(describes, {
|
||||||
|
name = describe_name,
|
||||||
|
start = start_idx - 1,
|
||||||
|
["end"] = end_idx - 1,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
local test_name = match_call_name(lines, i, { "test", "it" })
|
||||||
|
if test_name then
|
||||||
|
local start_idx = i
|
||||||
|
local end_idx = find_block_end(lines, start_idx)
|
||||||
|
table.insert(tests, {
|
||||||
|
name = test_name,
|
||||||
|
start = start_idx - 1,
|
||||||
|
["end"] = end_idx - 1,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function describe_chain(at_start)
|
||||||
|
local parents = {}
|
||||||
|
for _, describe in ipairs(describes) do
|
||||||
|
if at_start >= describe.start and at_start <= describe["end"] then
|
||||||
|
table.insert(parents, describe)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(parents, function(a, b)
|
||||||
|
if a.start == b.start then
|
||||||
|
return a["end"] < b["end"]
|
||||||
|
end
|
||||||
|
return a.start < b.start
|
||||||
|
end)
|
||||||
|
return parents
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, test in ipairs(tests) do
|
||||||
|
local parents = describe_chain(test.start)
|
||||||
|
local parts = {}
|
||||||
|
for _, parent in ipairs(parents) do
|
||||||
|
table.insert(parts, parent.name)
|
||||||
|
end
|
||||||
|
table.insert(parts, test.name)
|
||||||
|
test.full_name = table.concat(parts, "/")
|
||||||
|
test.jest_name = table.concat(parts, " ")
|
||||||
|
test.jest_parts = parts
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, describe in ipairs(describes) do
|
||||||
|
local parents = describe_chain(describe.start)
|
||||||
|
local parts = {}
|
||||||
|
for _, parent in ipairs(parents) do
|
||||||
|
table.insert(parts, parent.name)
|
||||||
|
end
|
||||||
|
describe.full_name = table.concat(parts, "/")
|
||||||
|
describe.jest_name = table.concat(parts, " ")
|
||||||
|
describe.jest_parts = parts
|
||||||
|
end
|
||||||
|
|
||||||
|
return tests, describes
|
||||||
|
end
|
||||||
|
|
||||||
|
local function test_file_path(path)
|
||||||
|
if not path or path == "" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return path:match("%.test%.[jt]sx?$") ~= nil
|
||||||
|
or path:match("%.spec%.[jt]sx?$") ~= nil
|
||||||
|
or path:match("%.test%.mjs$") ~= nil
|
||||||
|
or path:match("%.spec%.mjs$") ~= nil
|
||||||
|
or path:match("%.test%.cjs$") ~= nil
|
||||||
|
or path:match("%.spec%.cjs$") ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function collect_unique(list)
|
||||||
|
local out = {}
|
||||||
|
local seen = {}
|
||||||
|
for _, item in ipairs(list) do
|
||||||
|
if item and item ~= "" and not seen[item] then
|
||||||
|
seen[item] = true
|
||||||
|
table.insert(out, item)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
local function order_by_root(names)
|
||||||
|
local roots = {}
|
||||||
|
local seen_root = {}
|
||||||
|
local buckets = {}
|
||||||
|
|
||||||
|
for _, name in ipairs(names) do
|
||||||
|
local root = name:match("^[^/]+") or name
|
||||||
|
if not seen_root[root] then
|
||||||
|
seen_root[root] = true
|
||||||
|
table.insert(roots, root)
|
||||||
|
end
|
||||||
|
buckets[root] = buckets[root] or { main = nil, subs = {} }
|
||||||
|
if name == root then
|
||||||
|
buckets[root].main = name
|
||||||
|
else
|
||||||
|
table.insert(buckets[root].subs, name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local ordered = {}
|
||||||
|
for _, root in ipairs(roots) do
|
||||||
|
local bucket = buckets[root]
|
||||||
|
if bucket.main then
|
||||||
|
table.insert(ordered, bucket.main)
|
||||||
|
end
|
||||||
|
for _, sub in ipairs(bucket.subs) do
|
||||||
|
table.insert(ordered, sub)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return ordered
|
||||||
|
end
|
||||||
|
|
||||||
|
local function order_with_display(names, display_map)
|
||||||
|
local ordered = order_by_root(names)
|
||||||
|
local display = {}
|
||||||
|
for _, name in ipairs(ordered) do
|
||||||
|
display[#display + 1] = display_map[name] or name
|
||||||
|
end
|
||||||
|
return ordered, display
|
||||||
|
end
|
||||||
|
|
||||||
|
local function split_output_lines(text)
|
||||||
|
if not text or text == "" then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
local lines = vim.split(text, "\n", { plain = true })
|
||||||
|
if #lines > 0 and lines[#lines] == "" then
|
||||||
|
table.remove(lines, #lines)
|
||||||
|
end
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
|
local function reporter_path()
|
||||||
|
local source = debug.getinfo(1, "S").source
|
||||||
|
if source:sub(1, 1) == "@" then
|
||||||
|
source = source:sub(2)
|
||||||
|
end
|
||||||
|
local dir = vim.fs.dirname(source)
|
||||||
|
return vim.fs.normalize(dir .. "/../../reporter/test_samurai_jest_reporter.js")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function base_cmd()
|
||||||
|
return {
|
||||||
|
"npx",
|
||||||
|
"jest",
|
||||||
|
"--testLocationInResults",
|
||||||
|
"--reporters",
|
||||||
|
reporter_path(),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function parse_result_line(line)
|
||||||
|
if not line or line == "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
if line:sub(1, #RESULT_PREFIX) ~= RESULT_PREFIX then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local payload = line:sub(#RESULT_PREFIX + 1)
|
||||||
|
local ok, data = pcall(vim.json.decode, payload)
|
||||||
|
if not ok or type(data) ~= "table" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
|
||||||
|
local function update_location_cache(name, data)
|
||||||
|
if not name or name == "" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local location = data.location
|
||||||
|
if type(location) ~= "table" or not data.file then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
runner._last_locations[name] = {
|
||||||
|
filename = data.file,
|
||||||
|
lnum = location.line or 1,
|
||||||
|
col = location.column or 1,
|
||||||
|
text = name,
|
||||||
|
}
|
||||||
|
if data.jestName and data.jestName ~= "" then
|
||||||
|
runner._last_jest_names[name] = data.jestName
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.is_test_file(bufnr)
|
||||||
|
local path = get_buf_path(bufnr)
|
||||||
|
return test_file_path(path)
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.find_nearest(bufnr, row, _col)
|
||||||
|
local path = get_buf_path(bufnr)
|
||||||
|
if not path or path == "" then
|
||||||
|
return nil, "no file name"
|
||||||
|
end
|
||||||
|
if not test_file_path(path) then
|
||||||
|
return nil, "not a jest test file"
|
||||||
|
end
|
||||||
|
local lines = get_buf_lines(bufnr)
|
||||||
|
local tests, describes = find_tests(lines)
|
||||||
|
local nearest = nil
|
||||||
|
for _, test in ipairs(tests) do
|
||||||
|
if row >= test.start and row <= test["end"] then
|
||||||
|
nearest = test
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not nearest then
|
||||||
|
local describe_match = nil
|
||||||
|
for _, describe in ipairs(describes) do
|
||||||
|
if row >= describe.start and row <= describe["end"] then
|
||||||
|
if not describe_match or describe.start >= describe_match.start then
|
||||||
|
describe_match = describe
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if describe_match then
|
||||||
|
local cwd = find_root(path, {
|
||||||
|
"package.json",
|
||||||
|
"jest.config.js",
|
||||||
|
"jest.config.ts",
|
||||||
|
"jest.config.cjs",
|
||||||
|
"jest.config.mjs",
|
||||||
|
"jest.config.json",
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
file = path,
|
||||||
|
cwd = cwd,
|
||||||
|
test_name = describe_match.name,
|
||||||
|
full_name = describe_match.full_name,
|
||||||
|
jest_name = describe_match.jest_name,
|
||||||
|
jest_parts = describe_match.jest_parts,
|
||||||
|
kind = "describe",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
local cwd = find_root(path, {
|
||||||
|
"package.json",
|
||||||
|
"jest.config.js",
|
||||||
|
"jest.config.ts",
|
||||||
|
"jest.config.cjs",
|
||||||
|
"jest.config.mjs",
|
||||||
|
"jest.config.json",
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
file = path,
|
||||||
|
cwd = cwd,
|
||||||
|
kind = "file",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
local cwd = find_root(path, {
|
||||||
|
"package.json",
|
||||||
|
"jest.config.js",
|
||||||
|
"jest.config.ts",
|
||||||
|
"jest.config.cjs",
|
||||||
|
"jest.config.mjs",
|
||||||
|
"jest.config.json",
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
file = path,
|
||||||
|
cwd = cwd,
|
||||||
|
test_name = nearest.name,
|
||||||
|
full_name = nearest.full_name,
|
||||||
|
jest_name = nearest.jest_name,
|
||||||
|
jest_parts = nearest.jest_parts,
|
||||||
|
kind = "test",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.build_command(spec)
|
||||||
|
local file = spec.file
|
||||||
|
if not file or file == "" then
|
||||||
|
return { cmd = base_cmd(), cwd = spec.cwd }
|
||||||
|
end
|
||||||
|
if spec.kind == "file" then
|
||||||
|
local cmd = base_cmd()
|
||||||
|
table.insert(cmd, "--runTestsByPath")
|
||||||
|
table.insert(cmd, file)
|
||||||
|
return { cmd = cmd, cwd = spec.cwd }
|
||||||
|
end
|
||||||
|
local cmd = base_cmd()
|
||||||
|
table.insert(cmd, "--runTestsByPath")
|
||||||
|
table.insert(cmd, file)
|
||||||
|
local ok, pattern = pcall(function()
|
||||||
|
if type(spec.jest_parts) == "table" and #spec.jest_parts > 0 then
|
||||||
|
if spec.kind == "describe" then
|
||||||
|
return build_jest_prefix_pattern(spec.jest_parts)
|
||||||
|
end
|
||||||
|
return build_jest_pattern(spec.jest_parts)
|
||||||
|
end
|
||||||
|
local name = spec.jest_name or spec.test_name
|
||||||
|
if name and name ~= "" then
|
||||||
|
return "^" .. escape_regex(name) .. "$"
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end)
|
||||||
|
if not ok then
|
||||||
|
pattern = nil
|
||||||
|
end
|
||||||
|
if pattern then
|
||||||
|
table.insert(cmd, "--testNamePattern")
|
||||||
|
table.insert(cmd, pattern)
|
||||||
|
end
|
||||||
|
return { cmd = cmd, cwd = spec.cwd }
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.build_file_command(bufnr)
|
||||||
|
local path = get_buf_path(bufnr)
|
||||||
|
local cwd = find_root(path, {
|
||||||
|
"package.json",
|
||||||
|
"jest.config.js",
|
||||||
|
"jest.config.ts",
|
||||||
|
"jest.config.cjs",
|
||||||
|
"jest.config.mjs",
|
||||||
|
"jest.config.json",
|
||||||
|
})
|
||||||
|
local cmd = base_cmd()
|
||||||
|
table.insert(cmd, "--runTestsByPath")
|
||||||
|
table.insert(cmd, path)
|
||||||
|
return { cmd = cmd, cwd = cwd }
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.build_all_command(bufnr)
|
||||||
|
local path = get_buf_path(bufnr)
|
||||||
|
local cwd = find_root(path, {
|
||||||
|
"package.json",
|
||||||
|
"jest.config.js",
|
||||||
|
"jest.config.ts",
|
||||||
|
"jest.config.cjs",
|
||||||
|
"jest.config.mjs",
|
||||||
|
"jest.config.json",
|
||||||
|
})
|
||||||
|
local cmd = base_cmd()
|
||||||
|
return { cmd = cmd, cwd = cwd }
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.build_failed_command(last_command, failures, _scope_kind)
|
||||||
|
if not failures or #failures == 0 then
|
||||||
|
if last_command and last_command.cmd then
|
||||||
|
return { cmd = last_command.cmd, cwd = last_command.cwd }
|
||||||
|
end
|
||||||
|
return { cmd = base_cmd() }
|
||||||
|
end
|
||||||
|
|
||||||
|
local pattern_parts = {}
|
||||||
|
for _, name in ipairs(failures or {}) do
|
||||||
|
if name and name ~= "" then
|
||||||
|
local jest_name = runner._last_jest_names[name] or to_jest_full_name(name)
|
||||||
|
table.insert(pattern_parts, "^" .. escape_regex(jest_name) .. "$")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local pattern = "(" .. table.concat(pattern_parts, "|") .. ")"
|
||||||
|
|
||||||
|
local cmd = {}
|
||||||
|
local skip_next = false
|
||||||
|
for _, arg in ipairs(last_command and last_command.cmd or {}) do
|
||||||
|
if skip_next then
|
||||||
|
skip_next = false
|
||||||
|
elseif arg == "--testNamePattern" then
|
||||||
|
skip_next = true
|
||||||
|
else
|
||||||
|
table.insert(cmd, arg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #cmd == 0 then
|
||||||
|
cmd = base_cmd()
|
||||||
|
end
|
||||||
|
table.insert(cmd, "--testNamePattern")
|
||||||
|
table.insert(cmd, pattern)
|
||||||
|
|
||||||
|
return {
|
||||||
|
cmd = cmd,
|
||||||
|
cwd = last_command and last_command.cwd or nil,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.parse_results(output)
|
||||||
|
runner._last_locations = {}
|
||||||
|
runner._last_jest_names = {}
|
||||||
|
if not output or output == "" then
|
||||||
|
return { passes = {}, failures = {}, skips = {}, display = { passes = {}, failures = {}, skips = {} } }
|
||||||
|
end
|
||||||
|
local passes = {}
|
||||||
|
local failures = {}
|
||||||
|
local skips = {}
|
||||||
|
local display = { passes = {}, failures = {}, skips = {} }
|
||||||
|
local pass_display = {}
|
||||||
|
local fail_display = {}
|
||||||
|
local skip_display = {}
|
||||||
|
local seen = {
|
||||||
|
passes = {},
|
||||||
|
failures = {},
|
||||||
|
skips = {},
|
||||||
|
}
|
||||||
|
for line in output:gmatch("[^\n]+") do
|
||||||
|
local data = parse_result_line(line)
|
||||||
|
if data and data.name and data.status then
|
||||||
|
local kind = STATUS_MAP[data.status]
|
||||||
|
if kind and not seen[kind][data.name] then
|
||||||
|
seen[kind][data.name] = true
|
||||||
|
table.insert(kind == "passes" and passes or kind == "failures" and failures or skips, data.name)
|
||||||
|
if kind == "passes" then
|
||||||
|
pass_display[data.name] = data.display or data.name
|
||||||
|
elseif kind == "failures" then
|
||||||
|
fail_display[data.name] = data.display or data.name
|
||||||
|
else
|
||||||
|
skip_display[data.name] = data.display or data.name
|
||||||
|
end
|
||||||
|
update_location_cache(data.name, data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
passes = collect_unique(passes)
|
||||||
|
failures = collect_unique(failures)
|
||||||
|
skips = collect_unique(skips)
|
||||||
|
passes, display.passes = order_with_display(passes, pass_display)
|
||||||
|
failures, display.failures = order_with_display(failures, fail_display)
|
||||||
|
skips, display.skips = order_with_display(skips, skip_display)
|
||||||
|
|
||||||
|
return { passes = passes, failures = failures, skips = skips, display = display }
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.output_parser()
|
||||||
|
runner._last_locations = {}
|
||||||
|
runner._last_jest_names = {}
|
||||||
|
return {
|
||||||
|
on_line = function(line, state)
|
||||||
|
local data = parse_result_line(line)
|
||||||
|
if not data or not data.name or not data.status then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local kind = STATUS_MAP[data.status]
|
||||||
|
if not kind then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
state.jest = state.jest or { failures_all = {}, failures_seen = {} }
|
||||||
|
local results = {
|
||||||
|
passes = {},
|
||||||
|
failures = {},
|
||||||
|
skips = {},
|
||||||
|
display = { passes = {}, failures = {}, skips = {} },
|
||||||
|
}
|
||||||
|
if kind == "passes" then
|
||||||
|
results.passes = { data.name }
|
||||||
|
results.display.passes = { data.display or data.name }
|
||||||
|
elseif kind == "failures" then
|
||||||
|
results.failures = { data.name }
|
||||||
|
results.display.failures = { data.display or data.name }
|
||||||
|
if not state.jest.failures_seen[data.name] then
|
||||||
|
state.jest.failures_seen[data.name] = true
|
||||||
|
table.insert(state.jest.failures_all, data.name)
|
||||||
|
end
|
||||||
|
results.failures_all = vim.deepcopy(state.jest.failures_all)
|
||||||
|
else
|
||||||
|
results.skips = { data.name }
|
||||||
|
results.display.skips = { data.display or data.name }
|
||||||
|
end
|
||||||
|
update_location_cache(data.name, data)
|
||||||
|
return results
|
||||||
|
end,
|
||||||
|
on_complete = function(output, state)
|
||||||
|
return nil
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.parse_test_output(output)
|
||||||
|
local out = {}
|
||||||
|
if not output or output == "" then
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
for line in output:gmatch("[^\n]+") do
|
||||||
|
local data = parse_result_line(line)
|
||||||
|
if data and data.name and data.output then
|
||||||
|
out[data.name] = out[data.name] or {}
|
||||||
|
if type(data.output) == "string" then
|
||||||
|
for _, item in ipairs(split_output_lines(data.output)) do
|
||||||
|
table.insert(out[data.name], item)
|
||||||
|
end
|
||||||
|
elseif type(data.output) == "table" then
|
||||||
|
for _, item in ipairs(data.output) do
|
||||||
|
if item and item ~= "" then
|
||||||
|
table.insert(out[data.name], item)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
function runner.collect_failed_locations(failures, _command, _scope_kind)
|
||||||
|
if type(failures) ~= "table" or #failures == 0 then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
local items = {}
|
||||||
|
local seen = {}
|
||||||
|
for _, name in ipairs(failures) do
|
||||||
|
local loc = runner._last_locations[name]
|
||||||
|
if loc then
|
||||||
|
local key = string.format("%s:%d:%d:%s", loc.filename or "", loc.lnum or 0, loc.col or 0, name)
|
||||||
|
if not seen[key] then
|
||||||
|
seen[key] = true
|
||||||
|
table.insert(items, loc)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return items
|
||||||
|
end
|
||||||
|
|
||||||
|
return runner
|
||||||
52
reporter/test_samurai_jest_reporter.js
Normal file
52
reporter/test_samurai_jest_reporter.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
class TestSamuraiJestReporter {
|
||||||
|
constructor(_globalConfig, _options) {}
|
||||||
|
|
||||||
|
onTestCaseResult(test, testCaseResult) {
|
||||||
|
const name = this.buildListingName(testCaseResult);
|
||||||
|
const jestName = this.buildJestName(testCaseResult);
|
||||||
|
const payload = {
|
||||||
|
name,
|
||||||
|
jestName,
|
||||||
|
status: testCaseResult.status,
|
||||||
|
file: test && test.path ? test.path : null,
|
||||||
|
location: testCaseResult.location || null,
|
||||||
|
output: this.buildOutput(testCaseResult),
|
||||||
|
};
|
||||||
|
process.stdout.write(`TSAMURAI_RESULT ${JSON.stringify(payload)}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildListingName(testCaseResult) {
|
||||||
|
const ancestors = Array.isArray(testCaseResult.ancestorTitles)
|
||||||
|
? testCaseResult.ancestorTitles
|
||||||
|
: [];
|
||||||
|
const title = testCaseResult.title ? String(testCaseResult.title) : "";
|
||||||
|
return [...ancestors, title].filter(Boolean).join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
buildJestName(testCaseResult) {
|
||||||
|
const ancestors = Array.isArray(testCaseResult.ancestorTitles)
|
||||||
|
? testCaseResult.ancestorTitles
|
||||||
|
: [];
|
||||||
|
const title = testCaseResult.title ? String(testCaseResult.title) : "";
|
||||||
|
return [...ancestors, title].filter(Boolean).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
buildOutput(testCaseResult) {
|
||||||
|
const lines = [];
|
||||||
|
const failures = Array.isArray(testCaseResult.failureMessages)
|
||||||
|
? testCaseResult.failureMessages
|
||||||
|
: [];
|
||||||
|
failures.forEach((message) => {
|
||||||
|
if (typeof message === "string" && message.length > 0) {
|
||||||
|
message.split("\n").forEach((line) => {
|
||||||
|
if (line !== "") {
|
||||||
|
lines.push(line);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = TestSamuraiJestReporter;
|
||||||
3
run_test.sh
Normal file
3
run_test.sh
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
nvim --headless -u tests/minimal_init.lua -c "PlenaryBustedDirectory tests" -c qa
|
||||||
15
tests/minimal_init.lua
Normal file
15
tests/minimal_init.lua
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
vim.cmd("set rtp^=.")
|
||||||
|
|
||||||
|
local data_path = vim.fn.stdpath("data")
|
||||||
|
local plenary_paths = {
|
||||||
|
data_path .. "/site/pack/packer/start/plenary.nvim",
|
||||||
|
data_path .. "/lazy/plenary.nvim",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path in ipairs(plenary_paths) do
|
||||||
|
if vim.fn.isdirectory(path) == 1 then
|
||||||
|
vim.cmd("set rtp^=" .. path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.cmd("runtime! plugin/plenary.vim")
|
||||||
409
tests/test_jest_runner_spec.lua
Normal file
409
tests/test_jest_runner_spec.lua
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
local runner = require("test-samurai-jest-runner")
|
||||||
|
|
||||||
|
local function get_reporter_path()
|
||||||
|
local source = debug.getinfo(runner.build_command, "S").source
|
||||||
|
if source:sub(1, 1) == "@" then
|
||||||
|
source = source:sub(2)
|
||||||
|
end
|
||||||
|
local dir = vim.fs.dirname(source)
|
||||||
|
return vim.fs.normalize(dir .. "/../../reporter/test_samurai_jest_reporter.js")
|
||||||
|
end
|
||||||
|
|
||||||
|
describe("test-samurai-jest-runner", function()
|
||||||
|
it("detects Jest test files by suffix", function()
|
||||||
|
local bufnr1 = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_name(bufnr1, "/tmp/example.test.js")
|
||||||
|
assert.is_true(runner.is_test_file(bufnr1))
|
||||||
|
|
||||||
|
local bufnr2 = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_name(bufnr2, "/tmp/example.spec.tsx")
|
||||||
|
assert.is_true(runner.is_test_file(bufnr2))
|
||||||
|
|
||||||
|
local bufnr3 = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_name(bufnr3, "/tmp/example.js")
|
||||||
|
assert.is_false(runner.is_test_file(bufnr3))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("finds nearest test with describe hierarchy", function()
|
||||||
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_name(bufnr, "/tmp/math.test.js")
|
||||||
|
local lines = {
|
||||||
|
"describe('Math', () => {",
|
||||||
|
" test('adds', () => {",
|
||||||
|
" expect(1 + 1).toBe(2)",
|
||||||
|
" })",
|
||||||
|
"})",
|
||||||
|
}
|
||||||
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||||
|
|
||||||
|
local orig_fs_find = vim.fs.find
|
||||||
|
vim.fs.find = function(_, _)
|
||||||
|
return { "/tmp/package.json" }
|
||||||
|
end
|
||||||
|
|
||||||
|
local spec, err = runner.find_nearest(bufnr, 2, 0)
|
||||||
|
|
||||||
|
vim.fs.find = orig_fs_find
|
||||||
|
|
||||||
|
assert.is_nil(err)
|
||||||
|
assert.equals("adds", spec.test_name)
|
||||||
|
assert.equals("Math/adds", spec.full_name)
|
||||||
|
assert.equals("Math adds", spec.jest_name)
|
||||||
|
assert.are.same({ "Math", "adds" }, spec.jest_parts)
|
||||||
|
assert.is_true(spec.file:match("math%.test%.js$") ~= nil)
|
||||||
|
assert.equals("/tmp", spec.cwd)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("handles multiline describe and it declarations", function()
|
||||||
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_name(bufnr, "/tmp/multiline.test.js")
|
||||||
|
local lines = {
|
||||||
|
"describe(",
|
||||||
|
" '<Header/>',",
|
||||||
|
" () => {",
|
||||||
|
" describe(",
|
||||||
|
" 'renders properly...',",
|
||||||
|
" () => {",
|
||||||
|
" it(",
|
||||||
|
" 'the teaser links',",
|
||||||
|
" async () => {",
|
||||||
|
" expect(true).toBe(true)",
|
||||||
|
" }",
|
||||||
|
" )",
|
||||||
|
" }",
|
||||||
|
" )",
|
||||||
|
" }",
|
||||||
|
")",
|
||||||
|
}
|
||||||
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||||
|
|
||||||
|
local orig_fs_find = vim.fs.find
|
||||||
|
vim.fs.find = function(_, _)
|
||||||
|
return { "/tmp/package.json" }
|
||||||
|
end
|
||||||
|
|
||||||
|
local spec, err = runner.find_nearest(bufnr, 9, 0)
|
||||||
|
|
||||||
|
vim.fs.find = orig_fs_find
|
||||||
|
|
||||||
|
assert.is_nil(err)
|
||||||
|
assert.equals("the teaser links", spec.test_name)
|
||||||
|
assert.equals("<Header/>/renders properly.../the teaser links", spec.full_name)
|
||||||
|
assert.are.same({ "<Header/>", "renders properly...", "the teaser links" }, spec.jest_parts)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("uses describe block when cursor is between tests", function()
|
||||||
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_name(bufnr, "/tmp/between.test.js")
|
||||||
|
local lines = {
|
||||||
|
"describe('<Header/>', () => {",
|
||||||
|
" describe('renders properly...', () => {",
|
||||||
|
" it('the logo', async () => {",
|
||||||
|
" expect(true).toBe(true)",
|
||||||
|
" })",
|
||||||
|
" ",
|
||||||
|
" it('the teaser links', async () => {",
|
||||||
|
" expect(true).toBe(true)",
|
||||||
|
" })",
|
||||||
|
" })",
|
||||||
|
"})",
|
||||||
|
}
|
||||||
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||||
|
|
||||||
|
local orig_fs_find = vim.fs.find
|
||||||
|
vim.fs.find = function(_, _)
|
||||||
|
return { "/tmp/package.json" }
|
||||||
|
end
|
||||||
|
|
||||||
|
local spec, err = runner.find_nearest(bufnr, 5, 0)
|
||||||
|
|
||||||
|
vim.fs.find = orig_fs_find
|
||||||
|
|
||||||
|
assert.is_nil(err)
|
||||||
|
assert.equals("renders properly...", spec.test_name)
|
||||||
|
assert.equals("<Header/>/renders properly...", spec.full_name)
|
||||||
|
assert.are.same({ "<Header/>", "renders properly..." }, spec.jest_parts)
|
||||||
|
assert.equals("describe", spec.kind)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("falls back to file command when outside any describe", function()
|
||||||
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_name(bufnr, "/tmp/outside.test.js")
|
||||||
|
local lines = {
|
||||||
|
"const value = 1",
|
||||||
|
"function helper() { return value }",
|
||||||
|
}
|
||||||
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||||
|
|
||||||
|
local orig_fs_find = vim.fs.find
|
||||||
|
vim.fs.find = function(_, _)
|
||||||
|
return { "/tmp/package.json" }
|
||||||
|
end
|
||||||
|
|
||||||
|
local spec, err = runner.find_nearest(bufnr, 1, 0)
|
||||||
|
assert.is_nil(err)
|
||||||
|
assert.equals("file", spec.kind)
|
||||||
|
|
||||||
|
local cmd_spec = runner.build_command(spec)
|
||||||
|
|
||||||
|
vim.fs.find = orig_fs_find
|
||||||
|
|
||||||
|
assert.are.same(
|
||||||
|
{
|
||||||
|
"npx",
|
||||||
|
"jest",
|
||||||
|
"--testLocationInResults",
|
||||||
|
"--reporters",
|
||||||
|
get_reporter_path(),
|
||||||
|
"--runTestsByPath",
|
||||||
|
cmd_spec.cmd[#cmd_spec.cmd],
|
||||||
|
},
|
||||||
|
cmd_spec.cmd
|
||||||
|
)
|
||||||
|
assert.is_true(cmd_spec.cmd[#cmd_spec.cmd]:match("outside%.test%.js$") ~= nil)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("build_command uses npx jest with reporter and pattern", function()
|
||||||
|
local spec = {
|
||||||
|
file = "/tmp/math.test.js",
|
||||||
|
cwd = "/tmp",
|
||||||
|
full_name = "Math/adds",
|
||||||
|
jest_parts = { "Math", "adds" },
|
||||||
|
}
|
||||||
|
local cmd_spec = runner.build_command(spec)
|
||||||
|
assert.are.same(
|
||||||
|
{
|
||||||
|
"npx",
|
||||||
|
"jest",
|
||||||
|
"--testLocationInResults",
|
||||||
|
"--reporters",
|
||||||
|
get_reporter_path(),
|
||||||
|
"--runTestsByPath",
|
||||||
|
"/tmp/math.test.js",
|
||||||
|
"--testNamePattern",
|
||||||
|
"^.*adds$",
|
||||||
|
},
|
||||||
|
cmd_spec.cmd
|
||||||
|
)
|
||||||
|
assert.equals("/tmp", cmd_spec.cwd)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("build_command uses prefix pattern for describe blocks", function()
|
||||||
|
local spec = {
|
||||||
|
file = "/tmp/math.test.js",
|
||||||
|
cwd = "/tmp",
|
||||||
|
kind = "describe",
|
||||||
|
jest_parts = { "<Header/>", "renders properly..." },
|
||||||
|
}
|
||||||
|
local cmd_spec = runner.build_command(spec)
|
||||||
|
local pattern = cmd_spec.cmd[#cmd_spec.cmd]
|
||||||
|
assert.are.same(
|
||||||
|
{
|
||||||
|
"npx",
|
||||||
|
"jest",
|
||||||
|
"--testLocationInResults",
|
||||||
|
"--reporters",
|
||||||
|
get_reporter_path(),
|
||||||
|
"--runTestsByPath",
|
||||||
|
"/tmp/math.test.js",
|
||||||
|
"--testNamePattern",
|
||||||
|
pattern,
|
||||||
|
},
|
||||||
|
cmd_spec.cmd
|
||||||
|
)
|
||||||
|
assert.is_true(pattern:find("Header", 1, true) ~= nil)
|
||||||
|
assert.is_true(pattern:find("renders", 1, true) ~= nil)
|
||||||
|
assert.is_true(pattern:find("properly", 1, true) ~= nil)
|
||||||
|
assert.is_true(pattern:sub(-2) == ".*")
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("build_file_command scopes to file", function()
|
||||||
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_name(bufnr, "/tmp/project/foo.test.ts")
|
||||||
|
|
||||||
|
local orig_fs_find = vim.fs.find
|
||||||
|
vim.fs.find = function(_, _)
|
||||||
|
return { "/tmp/project/package.json" }
|
||||||
|
end
|
||||||
|
|
||||||
|
local cmd_spec = runner.build_file_command(bufnr)
|
||||||
|
|
||||||
|
vim.fs.find = orig_fs_find
|
||||||
|
|
||||||
|
assert.are.same(
|
||||||
|
{
|
||||||
|
"npx",
|
||||||
|
"jest",
|
||||||
|
"--testLocationInResults",
|
||||||
|
"--reporters",
|
||||||
|
get_reporter_path(),
|
||||||
|
"--runTestsByPath",
|
||||||
|
"/tmp/project/foo.test.ts",
|
||||||
|
},
|
||||||
|
cmd_spec.cmd
|
||||||
|
)
|
||||||
|
assert.equals("/tmp/project", cmd_spec.cwd)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("build_all_command runs project tests", function()
|
||||||
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_name(bufnr, "/tmp/project/bar.test.ts")
|
||||||
|
|
||||||
|
local orig_fs_find = vim.fs.find
|
||||||
|
vim.fs.find = function(_, _)
|
||||||
|
return { "/tmp/project/package.json" }
|
||||||
|
end
|
||||||
|
|
||||||
|
local cmd_spec = runner.build_all_command(bufnr)
|
||||||
|
|
||||||
|
vim.fs.find = orig_fs_find
|
||||||
|
|
||||||
|
assert.are.same(
|
||||||
|
{
|
||||||
|
"npx",
|
||||||
|
"jest",
|
||||||
|
"--testLocationInResults",
|
||||||
|
"--reporters",
|
||||||
|
get_reporter_path(),
|
||||||
|
},
|
||||||
|
cmd_spec.cmd
|
||||||
|
)
|
||||||
|
assert.equals("/tmp/project", cmd_spec.cwd)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("build_failed_command narrows to failed tests", function()
|
||||||
|
local last_command = {
|
||||||
|
cmd = {
|
||||||
|
"npx",
|
||||||
|
"jest",
|
||||||
|
"--testLocationInResults",
|
||||||
|
"--reporters",
|
||||||
|
get_reporter_path(),
|
||||||
|
"--runTestsByPath",
|
||||||
|
"/tmp/math.test.js",
|
||||||
|
"--testNamePattern",
|
||||||
|
"^Old$",
|
||||||
|
},
|
||||||
|
cwd = "/tmp",
|
||||||
|
}
|
||||||
|
local failures = { "Math/adds", "edge (1+1)" }
|
||||||
|
runner.parse_results(table.concat({
|
||||||
|
"TSAMURAI_RESULT " .. vim.json.encode({
|
||||||
|
name = "Math/adds",
|
||||||
|
jestName = "Math adds",
|
||||||
|
status = "failed",
|
||||||
|
}),
|
||||||
|
"TSAMURAI_RESULT " .. vim.json.encode({
|
||||||
|
name = "edge (1+1)",
|
||||||
|
jestName = "edge (1+1)",
|
||||||
|
status = "failed",
|
||||||
|
}),
|
||||||
|
}, "\n"))
|
||||||
|
|
||||||
|
local cmd_spec = runner.build_failed_command(last_command, failures, "file")
|
||||||
|
|
||||||
|
local pattern = cmd_spec.cmd[#cmd_spec.cmd]
|
||||||
|
assert.are.same(
|
||||||
|
{
|
||||||
|
"npx",
|
||||||
|
"jest",
|
||||||
|
"--testLocationInResults",
|
||||||
|
"--reporters",
|
||||||
|
get_reporter_path(),
|
||||||
|
"--runTestsByPath",
|
||||||
|
"/tmp/math.test.js",
|
||||||
|
"--testNamePattern",
|
||||||
|
pattern,
|
||||||
|
},
|
||||||
|
cmd_spec.cmd
|
||||||
|
)
|
||||||
|
assert.is_true(pattern:match("%^Math adds%$") ~= nil)
|
||||||
|
assert.is_true(pattern:match("edge") ~= nil)
|
||||||
|
assert.is_true(pattern:find("\\(1", 1, true) ~= nil)
|
||||||
|
assert.is_true(pattern:find("\\+1", 1, true) ~= nil)
|
||||||
|
assert.equals("/tmp", cmd_spec.cwd)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("parse_results collects statuses and locations", function()
|
||||||
|
local output = table.concat({
|
||||||
|
"TSAMURAI_RESULT " .. vim.json.encode({
|
||||||
|
name = "Math/adds",
|
||||||
|
status = "passed",
|
||||||
|
file = "/tmp/math.test.js",
|
||||||
|
location = { line = 3, column = 4 },
|
||||||
|
}),
|
||||||
|
"TSAMURAI_RESULT " .. vim.json.encode({
|
||||||
|
name = "Math/subtracts",
|
||||||
|
status = "failed",
|
||||||
|
file = "/tmp/math.test.js",
|
||||||
|
location = { line = 10, column = 2 },
|
||||||
|
}),
|
||||||
|
"TSAMURAI_RESULT " .. vim.json.encode({
|
||||||
|
name = "Math/skipped",
|
||||||
|
status = "skipped",
|
||||||
|
file = "/tmp/math.test.js",
|
||||||
|
location = { line = 20, column = 2 },
|
||||||
|
}),
|
||||||
|
}, "\n")
|
||||||
|
|
||||||
|
local results = runner.parse_results(output)
|
||||||
|
assert.are.same({ "Math/adds" }, results.passes)
|
||||||
|
assert.are.same({ "Math/subtracts" }, results.failures)
|
||||||
|
assert.are.same({ "Math/skipped" }, results.skips)
|
||||||
|
assert.are.same({ "Math/adds" }, results.display.passes)
|
||||||
|
assert.are.same({ "Math/subtracts" }, results.display.failures)
|
||||||
|
assert.are.same({ "Math/skipped" }, results.display.skips)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("output_parser streams per test case", function()
|
||||||
|
local parser = runner.output_parser()
|
||||||
|
local state = {}
|
||||||
|
local line = "TSAMURAI_RESULT " .. vim.json.encode({
|
||||||
|
name = "Math/adds",
|
||||||
|
status = "failed",
|
||||||
|
file = "/tmp/math.test.js",
|
||||||
|
location = { line = 3, column = 4 },
|
||||||
|
})
|
||||||
|
|
||||||
|
local results = parser.on_line(line, state)
|
||||||
|
|
||||||
|
assert.are.same({ "Math/adds" }, results.failures)
|
||||||
|
assert.are.same({ "Math/adds" }, results.display.failures)
|
||||||
|
assert.are.same({ "Math/adds" }, results.failures_all)
|
||||||
|
assert.is_nil(parser.on_complete("", state))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("parse_test_output groups output per test", function()
|
||||||
|
local output = table.concat({
|
||||||
|
"TSAMURAI_RESULT " .. vim.json.encode({
|
||||||
|
name = "Math/adds",
|
||||||
|
status = "failed",
|
||||||
|
output = { "line1", "line2" },
|
||||||
|
}),
|
||||||
|
"TSAMURAI_RESULT " .. vim.json.encode({
|
||||||
|
name = "Math/adds",
|
||||||
|
status = "failed",
|
||||||
|
output = { "line3" },
|
||||||
|
}),
|
||||||
|
}, "\n")
|
||||||
|
|
||||||
|
local results = runner.parse_test_output(output)
|
||||||
|
assert.are.same({ "line1", "line2", "line3" }, results["Math/adds"])
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("collect_failed_locations uses cached locations", function()
|
||||||
|
local output = "TSAMURAI_RESULT " .. vim.json.encode({
|
||||||
|
name = "Math/adds",
|
||||||
|
status = "failed",
|
||||||
|
file = "/tmp/math.test.js",
|
||||||
|
location = { line = 8, column = 2 },
|
||||||
|
})
|
||||||
|
runner.parse_results(output)
|
||||||
|
|
||||||
|
local items = runner.collect_failed_locations({ "Math/adds" }, nil, "file")
|
||||||
|
assert.equals(1, #items)
|
||||||
|
assert.equals("/tmp/math.test.js", items[1].filename)
|
||||||
|
assert.equals(8, items[1].lnum)
|
||||||
|
assert.equals(2, items[1].col)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
Reference in New Issue
Block a user