Runs your Jest tests in Dagger, one check per project, with every test exported to OpenTelemetry as a span.
Requires Dagger v1.0.0-beta.15 or later.
dagger install github.com/dagger/jestdagger check # every Jest project visible from here
dagger check -l --all # one line per project and test file
dagger check --jest --jest-project=web
dagger check --jest --jest-project=web --jest-test-file=src/App.test.jsx
dagger check jest/projects/tests/test --jest-test-file=src/App.test.jsx
dagger check -l --all --jest -f=cli # each line as flags to reuse
dagger list jest-projects -a
dagger list jest-test-files -a --jest-project=webThe toolchain has one check, test, at jest/projects/tests/test. It runs
once per selected project, over that project's selected test files:
- With every test file of the project selected, it runs
jestwith no file arguments, so Jest's own configuration decides what runs. - With some filtered out, it runs
jest --passWithNoTests --runTestsByPath <files>over the selected files only.
dagger check -l without --all prints a single row with * in the
JEST-PROJECT and JEST-TEST-FILE columns. The * stands for every
project and test file in view; add --all to list them one per row.
Use dagger check to run tests, in CI too: it fails when a test fails.
| Flag | Selects |
|---|---|
--jest, --by-jest |
checks from this module |
--check test |
checks named test, in every installed module |
--jest-project=PATH |
one project, by root relative to the workspace root (repeatable) |
--jest-projects |
every project |
--jest-test-file=PATH |
one test file, by path relative to its project (repeatable) |
--jest-tests |
every test file |
dagger check --help lists the flags in effect. They can change when another
installed module has a JestProject or JestTestFile type too.
Each project is its own check. A failure names the project and the step that failed, with the end of its output:
Jest project a: install failed (npm install, exit 1):
npm error ...
Jest project b: jest failed (exit 1):
FAIL src/sum.test.js
...
Jest project c: jest is not installed: add it to the devDependencies of c/package.json (...)
A project is a directory holding a jest.config.js, .mjs, .cjs, .ts,
.mts, .cts or .json: the names Jest loads on its own when run there.
node_modules is not searched. Keys are relative to the workspace root.
- A
jestkey inpackage.jsonalso configures Jest, butpackage.jsonmarks every npm package, so it is not a marker. - A named variant such as
jest.config-eslint-7.jsis not a marker either: Jest never loads it by itself, only through--configor aprojectsentry, so its directory is not a project Jest would run on its own. It is still used when a root config'sprojectsnames it (below).
Which projects you see depends on where you run dagger:
- Inside a project's subdirectory: the enclosing project, plus any projects nested below that directory.
- At a project root: that project and the projects below it, never the ones above it.
- In a directory that belongs to no project: the projects below it.
Given web/jest.config.js and api/jest.config.js:
dagger check # runs web and api
cd web/src && dagger check # runs web only
cd web && dagger list jest-projects -a # webWhen a project's config lists projects as string literals, for example
module.exports = {
projects: ["<rootDir>/packages/*/jest.config.js"],
};the projects it names are run by it, so they are not keyed again: dagger check from the root runs the root project once, and Jest runs every package
through it. Their test files are keyed under the root project
(packages/a/src/a.test.js) and read with each package's own config. From
inside a package (cd packages/a && dagger check) the package is a project of
its own and runs alone. A projects list that is not all string literals is
not read, and its nested projects are keyed on their own as well.
Test files are found in one search of the project, with node_modules and
git-ignored files skipped; listing runs no container and no Jest. They are
the files the project's Jest config would run, read statically from its
config file:
testMatch,testRegex,testPathIgnorePatterns,rootsandrootDirare honoured when they are literal strings or arrays of them.- Anything else, and a config that exports something other than an object
literal (a function call such as
module.exports = buildConfig(...), an import), falls back to Jest's defaults:testMatch**/__tests__/**/*.?([mc])[jt]s?(x)and**/?(*.)+(spec|test).?([mc])[jt]s?(x), ignoring/node_modules/. - Nested projects report their own files, unless a
projectslist names them.
So static discovery can be wrong about a config it cannot read:
- It may list a helper that the config excludes. Selecting only that file runs nothing and passes.
- It may miss a test that only the config's own logic finds. That test still runs whenever the whole project runs, because that run uses Jest's config.
- A project with no test file it can find reports no test files, so
dagger checkdoes not run it; call itstestfunction from a module instead.
Jest runs from the project directory.
Without a package.json at or above the project, npx fetches Jest.
With one, dependencies are installed and the project's own Jest runs: the
nearest node_modules/.bin/jest between the project and the install root, or
yarn's under Plug'n'Play. If there is none, the check fails with jest is not installed: add it to the devDependencies of ... rather than fetching a
different version.
- Install root. The nearest workspace root at or above the project: a
directory with
pnpm-workspace.yaml, or apackage.jsonwith"workspaces". Failing that, the nearest lockfile's directory, then the nearestpackage.json's. A package inside a monorepo therefore installs with the whole workspace and sees the files above it, such as a sharedjest.base.config.js. - Package manager. The
packageManagersetting if set. Otherwise thepackageManagerfield of the install root'spackage.json, then its lockfile (pnpm-lock.yamlorpnpm-workspace.yaml: pnpm,yarn.lock: yarn,bun.lock/bun.lockb: bun), then npm. pnpm and yarn run through corepack, installed when the image lacks it, at the version thepackageManagerfield pins. - Caching. The install sees only what it reads: every
package.json, lockfiles,pnpm-workspace.yaml,.npmrc,.yarnrc*,.yarn/{releases,plugins,patches},.pnpmfile.cjs,bunfig.tomlandpatches/, plus the directories of local dependencies (file:,link:,portal:specs), which the install copies or links, and the files workspace packages name in"bin", which it links intonode_modules/.bin. The rest of the source is laid over the result, so editing a source file does not reinstall. With pnpm'sdependenciesMetainjected, or a local dependency outside the install root, the install gets the whole source instead. Package manager caches, the pnpm store and corepack live on cache volumes. - Less noise. Browser downloads (Playwright, Puppeteer, Cypress) and git hook installers (husky, simple-git-hooks) are switched off.
- Install scripts still run, but they see only the install inputs, so a
postinstallorpreparescript that builds from source fails. PassinstallFlags = ["--ignore-scripts"], and setbuild = trueif the tests need the build output.
The install root, or the project itself, is mounted at /src without
node_modules and without files ignored by .gitignore.
With build set, the package manager's run build runs in the project
before the tests, when the nearest package.json at or above the project has
a build script; a package without one just runs its tests. The build runs
only in the selected project: running packages/app alone does not build the
workspace packages it depends on, so give it a build script that builds
them first, or run from the workspace root, whose own build script usually
builds every package.
Set them with dagger settings, or in dagger.toml:
dagger settings jest environment TZ=UTC
dagger settings jest installFlags -- --ignore-scripts # "--" before a value that starts with -
dagger settings -u jest installFlags # back to the default[modules.jest.settings]
baseImageAddress = "node:22-alpine" # default: node:25-alpine; any image with node and npm
packageManager = "pnpm" # default: "" (detect); npm, yarn, pnpm or bun
installFlags = ["--ignore-scripts"] # default: []; appended to the install command
environment = ["TZ=UTC"] # default: []; KEY=VALUE for the build and the tests
build = true # default: false; run the build script before testing
useEnv = true # default: false; use the project's own Jest environment
flags = ["--ci"] # default: []; flags passed to every jest runUnless useEnv is set, the toolchain preloads its register hook through
NODE_OPTIONS, so tests are traced without any change to your config. With
useEnv, the toolchain's build of @dagger.io/jest is linked into
node_modules, for a config that names @dagger.io/jest/node-environment.
projects(ws) returns a collection. Build its members with get(key:), narrow
it with subset(keys:), and reach the functions that span the collection
through batch:
let projects = jest.projects(ws)
projects.keys # ["api", "web"]
projects.batch.test(ws) # run every project, list the failures
projects.get(key: "web").test(ws) # run one whole project
projects.get(key: "web").list(ws) # jest --listTests output
projects.get(key: "web").installRoot(ws) # where dependencies are installed
let files = projects.get(key: "web").tests(ws)
files.keys # ["src/App.test.jsx", ...]
run(files.subset(keys: ["src/App.test.jsx"]).batch.test(ws))
run(files.get(key: "src/App.test.jsx").test(ws))
JestProjects.test and JestProject.test are plain functions. They are not
checks, so that dagger check does not run the same tests twice. The
test-file test functions are checks. A check called through a dependency
comes back unrun, so wrap it in a helper that runs it:
let run(check: Check!): Void {
if (check.pass == false) {
raise check.error.message ?? "check failed"
}
null
}
Settings are constructor arguments:
jest(build: true, installFlags: ["--ignore-scripts"]).projects(ws).
From the command line, dagger call cannot select a collection item yet; the
Dagger shell can:
dagger -c 'jest | projects | get web | list'The end-to-end tests in .dagger/modules/e2e exercise all of this.
Automatically instrument Jest tests for Open Telemetry.
The toolchain does this automatically, however you can use the library without the toolchain as described below.
Test spans include dagger.io/ui.boundary plus OpenTelemetry test semantic convention attributes: test.case.name, test.case.result.status, and test.suite.name.
Suite spans include dagger.io/ui.boundary, test.suite.name, and test.suite.run.status.
Console output emitted with console.log, console.info, console.debug, console.warn,
and console.error is exported as OpenTelemetry logs on the active test span.
Install @dagger.io/jest in your project
npm install @dagger.io/jestYou can either follow a no-configuration setup or update your current jest.config.js file.
Add the following import in your NODE_OPTIONS to auto-instrument when running your test
"NODE_OPTIONS=\"$NODE_OPTIONS --require @dagger.io/jest/register \" jest💡 If your project is in ESM, make sure you first followed ECMAScript Module setup on Jest
The library export an environment that you can use to automatically instrument your tests:
testEnvironment: "@dagger.io/jest/node-environment"