Skip to content
Join Now Login

Fix jest-ctrf-json-reporter ENOENT: CTRF Guide

CTRF (Common Test Report Format) is a universal JSON schema for test results.

Why does jest-ctrf-json-reporter throw ENOENT?

Section titled “Why does jest-ctrf-json-reporter throw ENOENT?”

Because a directory path was put inside outputFile. The reporter writes to path.join(outputDir, outputFile) and only creates outputDir (with mkdirSync(..., { recursive: true })), so any directory segments embedded in outputFile are never created and the write fails with ENOENT: no such file or directory. The fix is to move the directory into outputDir and keep outputFile a bare filename.

jest.config.js
['jest-ctrf-json-reporter', { outputFile: 'ctrf/coverage/report.json' }] // ENOENT
['jest-ctrf-json-reporter', { outputDir: 'ctrf/coverage', outputFile: 'report.json' }] // works

Both paths are resolved relative to the process working directory, which is your repo root under npm test but not necessarily inside a container or a monorepo package. There is no CTRF_OUTPUT_PATH environment variable in any CTRF reporter: outputDir in the config is the only supported way to move the file. playwright-ctrf-json-reporter and cypress-ctrf-json-reporter behave the same way. Full troubleshooting below.

CTRF gives you one report shape across every testing tool, framework, and language you run.

  • Universal: Works with any test framework that has a CTRF reporter
  • Consistent: Same format across Jest, Playwright, pytest, RSpec, and more
  • Rich data: Includes timing, retries, flaky test detection, and metadata
  • Open standard: Community-driven, MIT-licensed specification at ctrf.io

CTRF has reporters for most popular test frameworks:

FrameworkPackageLanguage
Playwrightplaywright-ctrf-json-reporterJavaScript/TypeScript
Jestjest-ctrf-json-reporterJavaScript/TypeScript
Vitestvitest-ctrf-json-reporterJavaScript/TypeScript
Cypresscypress-ctrf-json-reporterJavaScript/TypeScript
Mochamocha-ctrf-json-reporterJavaScript/TypeScript
pytestpytest-ctrfPython
Goctrf-go-json-reporterGo
JUnit XMLjunit-to-ctrfAny (CLI converter, not a reporter)

See the full list at ctrf.io.

Terminal window
npm install playwright-ctrf-json-reporter --save-dev
playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [
['playwright-ctrf-json-reporter', { outputDir: '.', outputFile: 'ctrf-report.json' }],
['list'], // Also show in console
],
});

The reporter writes to <outputDir>/<outputFile>. outputDir defaults to ctrf, so setting it to . puts the report at ./ctrf-report.json, which is the path the upload examples below use.

Terminal window
npm install jest-ctrf-json-reporter --save-dev
jest.config.js
module.exports = {
reporters: [
'default',
['jest-ctrf-json-reporter', { outputDir: '.', outputFile: 'ctrf-report.json' }],
],
};
Terminal window
npm install vitest-ctrf-json-reporter --save-dev
vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
reporters: [
'default',
['vitest-ctrf-json-reporter', { outputDir: '.', outputFile: 'ctrf-report.json' }],
],
},
});
Terminal window
pip install pytest-ctrf
Terminal window
pytest --ctrf ctrf-report.json

ENOENT: no such file or directory writing the CTRF report

Section titled “ENOENT: no such file or directory writing the CTRF report”
ENOENT: no such file or directory, open 'ctrf/coverage/report.json'

The cause is putting a directory path inside outputFile. The JS reporters take two separate options and write to path.join(outputDir, outputFile). They create outputDir with mkdirSync(outputDir, { recursive: true }), but nothing creates the directory segments you embed in outputFile, so the write lands in a directory that never got made.

jest.config.js
// ❌ Wrong: resolves to ctrf/coverage/report.json (default outputDir is `ctrf`),
// and the coverage/ segment is never created
['jest-ctrf-json-reporter', { outputFile: 'coverage/report.json' }]
// ✅ Correct: outputDir is created recursively, outputFile is just the filename
['jest-ctrf-json-reporter', { outputDir: 'ctrf/coverage', outputFile: 'report.json' }]

playwright-ctrf-json-reporter and cypress-ctrf-json-reporter behave identically, including the ctrf default for outputDir. vitest-ctrf-json-reporter uses the same option shape with different defaults (vitest-ctrf/report.json). If a reporter’s README doesn’t document outputDir, add a mkdir -p step before the test run and keep outputFile a plain filename.

The same rule explains the other half of this error: with no options at all, the report is written to ctrf/ctrf-report.json, not ./ctrf-report.json. Point your upload step at the real path or set outputDir: '.'.

If the report is generated but Gaffer reports zero tests parsed, the reporter probably wasn’t invoked.

  • Default reporter overridden. The reporters array in jest.config.js (or the equivalent in your framework) replaces the default. Make sure both 'default' and the CTRF reporter are listed, otherwise tests run but no CTRF file is written.
  • Tests crashed before reporting. If the suite throws during setup (a beforeAll that fails to connect to a service, for example), some reporters bail before writing the file. Check the CI logs for the underlying error before assuming the reporter is broken.

Once you have a CTRF JSON file, upload it to Gaffer:

Terminal window
curl -X POST https://app.gaffer.sh/api/upload \
-H "X-API-Key: YOUR_PROJECT_TOKEN" \
-F 'tags={"commitSha":"abc123","branch":"main"}'
- name: Run tests
run: npm test
- name: Upload CTRF report to Gaffer
if: always()
uses: gaffer-sh/gaffer-uploader@v2
with:
gaffer_upload_token: ${{ secrets.GAFFER_PROJECT_TOKEN }}
report_path: ./ctrf-report.json
commit_sha: ${{ github.sha }}
branch: ${{ github.ref_name }}
test:
script:
- npm ci
- npm test
after_script:
- |
curl -X POST https://app.gaffer.sh/api/upload \
-H "X-API-Key: $GAFFER_PROJECT_TOKEN" \
-F 'tags={"commitSha":"'"$CI_COMMIT_SHA"'","branch":"'"$CI_COMMIT_REF_NAME"'"}'

A CTRF report contains standardized test result data:

{
"results": {
"tool": {
"name": "playwright"
},
"summary": {
"tests": 42,
"passed": 40,
"failed": 1,
"pending": 0,
"skipped": 1,
"other": 0,
"start": 1703520000000,
"stop": 1703520060000
},
"tests": [
{
"name": "should login successfully",
"status": "passed",
"duration": 1234,
"retries": 0,
"flaky": false
},
{
"name": "should display dashboard",
"status": "failed",
"duration": 5678,
"message": "Element not found: #dashboard"
}
]
}
}

When you upload CTRF reports to Gaffer, you get:

  • Structured analytics: Pass rates, duration trends, flaky test detection
  • Cross-framework comparison: Compare results from different test suites
  • Failure patterns: See which tests fail most frequently
  • Historical tracking: See how tests perform over time

CI Provider Guides:

Reference:

Gaffer’s free tier includes 500 MB of storage with 7-day retention. Upload your first CTRF report in under 5 minutes.

Start Uploading CTRF Reports - Free