Debugging a test starts not with a fix but with reproduction. The temptation is strong - seeing the cart test go red, to dive straight into the code and change something at random. But until the failure reproduces reliably, any edit is a shot in the dark: it removes the symptom without touching the cause. The discipline is simple - narrow to a single test, understand what it expected and what it got, then fix. Our case: the checkout test sometimes fails on a mismatched total, sometimes hangs Jest.
The first step is to remove the noise. When a file holds dozens of tests, separate the suspect. test.only runs only the marked test, skipping the rest; describe.only does the same for a whole block. The inverse tool is test.skip, which switches a test off temporarily without deleting it, while test.todo reserves a name for one not yet written, so it shows up as pending. only is easy to forget and commit - then CI runs one test instead of a thousand; the lint rule no-focused-tests catches it.
You can also narrow from the outside, from the command line, without touching the code. The -t flag (--testNamePattern) runs only tests whose describe/test name matches the pattern. A positional argument filters by file path: jest checkout runs only files whose path contains checkout. The two filters combine - jest checkout -t total narrows by both file and name. The result is the same as only, but without editing the source - handier for fast iteration.
--watch and --watchAll keep Jest running and rerun tests when a file is saved. The difference is scope: --watch runs only tests affected by changes since the last commit (it leans on git), while --watchAll reruns everything and needs no repository. Watch mode is interactive: p filters by a path pattern, t by name, f runs only the ones that failed last time, Enter repeats the run. This is the working loop of debugging: narrow with a key, fix, and see the result in seconds.
A red test tells you what happened - you have to read it. A matcher like toBe or toEqual prints an Expected / Received block: on top the expected, below the received, and for objects a diff of the differences. The stack trace under it points to the file and line where the assert fired; read it top to bottom to the first frame in your own code, not in Jest's internals. The --verbose flag expands the report to a line per test - showing which one failed. Jest does not swallow console.log: it prints it with the file and line.
When printing is not enough, you need a debugger with breakpoints. Jest is an ordinary Node process, so you launch it under the inspector: node --inspect-brk ./node_modules/.bin/jest --runInBand. The --inspect-brk flag halts execution on the first line and waits for a client - chrome://inspect or the VS Code debugger. The key point is --runInBand: by default Jest spreads tests across workers, and a breakpoint from the main process will not fire in them - --runInBand collapses everything into the process the inspector is attached to, or the breakpoints are not hit.
The second half of the case - Jest does not exit: 'a worker process has failed to exit gracefully'. The tests passed, but something is holding the event loop open - a timer, a socket, a database connection. The --detectOpenHandles flag tracks these resources and prints the stack of their creation, usually pointing at a setInterval or a db client forgotten in afterAll. The cure is closing the resource, not the --forceExit flag: that kills the process after the tests, hiding the leak as retry hides flakiness. Our culprit was a payment check timer; removed in afterEach, Jest exited on its own, and the total mismatch turned out to be a rounding of cents.
# Run only tests whose name matches a pattern
npx jest -t 'checkout total'
# Run only files whose path contains "checkout"
npx jest checkout
# Combine: narrow to file AND name
npx jest checkout -t total# Launch Jest under the Node inspector, single process
node --inspect-brk ./node_modules/.bin/jest --runInBand
# Then open chrome://inspect, or attach the VS Code debugger