| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
How many of your tests actually run?
Somebody writes user.spec.ts. Your Jest config says testMatch: ["**/*.test.ts"].
The file is never collected. The tests inside it have never run — not once, not in CI, not locally. The suite passes. Coverage does not drop, because uncovered files were never in the denominator. Code review sees a file full of tests and approves it.
Nothing anywhere reports this. There is no warning, no error, no summary line. Jest cannot tell you about a file it does not know exists.
The same thing happens four other ways:
Every one of these is invisible from a green build. That is what makes them expensive: the build is not just failing to catch bugs, it is actively reporting that it checked.
ghosttest counts the tests in your repository, counts the ones that will run, and tells you where the difference went.
$ ghosttest
13 tests found · 4 will run · 9 never will
█████████▎ 31%
5 in files never collected · 3 suppressed by a focused test · 1 skipped
runner: jest — collection rules read exactly from JSON (package.json)
error uncollected-test-file legacy/cart.test.js:1
2 tests in this file, and jest would not collect it — it is excluded by `/legacy/`.
so: These tests have never run. The suite passes without them, so nothing has
ever reported that this code is untested — which is worse than having no
tests, because the green tick says otherwise.
fix: Narrow the exclude pattern. Exclusion is applied after matching, so a
broad entry silently beats a correct include.
error focused-test-committed src/checkout.test.js:2
A focused test on line 2 silently switches off the other 3 tests in this file.Two numbers, side by side. That is the whole product.
npx ghosttestOr as a dev dependency:
npm install --save-dev ghosttestRequires Node 20.10 or newer. Zero dependencies — including the glob matcher, which had to be written by hand because Jest's default testMatch uses three extglob forms in one line.
ghosttest # current directory
ghosttest ./packages/api # somewhere else
ghosttest --verbose # list every test file and its case count
ghosttest --json # machine-readable
ghosttest --fail-on warning # stricter gateIn CI, before or after your tests — it does not need them to have run:
- run: npm ci
- run: npx ghosttestExit codes: 0 clean, 1 findings at or above the threshold, 2 bad usage.
| Runner | Rules read from |
|---|---|
| Jest | the jest field in package.json, jest.config.json (exact), or literal arrays in jest.config.{js,ts,mjs,cjs} |
| Vitest | literal include/exclude in vitest.config.* or vite.config.* |
| node --test | the glob in your test script |
Each runner's documented defaults are used when no configuration is found, and the report always says which of the two it did — "read exactly from JSON" and "assumed from the runner's defaults" support very different levels of confidence.
A test file is a file that contains tests. Not a file whose name matches a pattern. Defining it by name would inherit the exact bug the tool exists to find — user.spec.ts is a test file by any human standard, and it is precisely the one nobody is running. So every source file is read and scanned, and the ones with test cases in them become the census.
The cost of a .only is counted, not just flagged. Lint rules tell you not to commit one. ghosttest tells you that the one on line 2 switched off the other seventeen tests in that file. The second is what gets it fixed.
Nothing is executed. vitest.config.ts is a TypeScript module that can do anything; a tool that imports it to read two arrays has quietly become a tool that runs the repository it was pointed at. Config files are parsed as text. When a config is computed rather than literal, ghosttest says so and falls back to documented defaults instead of guessing.
| Rule | Severity | Meaning |
|---|---|---|
| uncollected-test-file | error | a file full of tests the runner never collects |
| focused-test-committed | error | a .only suppressing the rest of its file |
| test-without-assertion | warning | a test that runs and cannot fail |
| skipped-test-inventory | info | how many tests are skipped, and where |
Details, with worked examples of each: docs/rules.md.
The haunted fixture contains one small project with all four problems:
git clone https://github.com/hamodywe/ghosttest && cd ghosttest
npm install
node src/cli.ts test/fixtures/haunted # 13 tests, 4 run
node src/cli.ts test/fixtures/clean # silence$ npx ghosttest .
82 tests found · 82 will run
$ npm test
ℹ pass 82The census and the runner agree exactly. That is the check that matters for a counting tool, and it runs in CI on every commit.
A repository that deliberately contains test files nobody should run — this one does — declares them:
{ "ignore": ["test/fixtures/**"] }in ghosttest.config.json, or with a repeatable --ignore flag. There are no built-in exclusions for directories called fixtures: silently ignoring a directory is exactly the failure this tool exists to find, and building one in would be a poor joke.
Stated plainly.
Isn't this what jest --listTests does? --listTests prints what Jest collects. It cannot print what it should have collected, because it does not know those files exist. The value here is the comparison — and --listTests is a manual debugging step nobody runs on a schedule.
Doesn't ESLint catch the .only? eslint-plugin-jest/no-focused-tests and Biome's noFocusedTests do, if you have them configured for your test files — which many projects using Vitest do not. ghosttest also counts what the .only suppressed, which the lint rules do not.
Will it work without installing my project's dependencies? Yes. It reads files; it never invokes the runner.
Does it modify anything? No. It only reads.
The most useful issue is a wrong census — a test counted that does not exist, or a file reported as uncollected that your runner does in fact collect. See CONTRIBUTING.md.
See ROADMAP.md.
MIT © hamodywe
| Back | FazBrowse Home | New Git URL |