Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- `[jest-reporters]` Fix issue where console output not displayed for GHA reporter even with `silent: false` option ([#15864](https://github.com/jestjs/jest/pull/15864))
- `[jest-runtime]` Fix issue where user cannot utilize dynamic import despite specifying `--experimental-vm-modules` Node option ([#15842](https://github.com/jestjs/jest/pull/15842))
- `[jest-test-sequencer]` Fix issue where failed tests due to compilation errors not getting re-executed even with `--onlyFailures` CLI option ([#15851](https://github.com/jestjs/jest/pull/15851))
- `[jest-reporters]` apply global coverage threshold to unmatched pattern files in addition to glob/path thresholds ([#15879](https://github.com/jestjs/jest/pull/15879)), fixes ([#5247](https://github.com/jestjs/jest/issues/5427))

### Chore & Maintenance

Expand Down
48 changes: 43 additions & 5 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,11 @@ For more information about the options object shape refer to `CoverageReporterWi

Default: `undefined`

This will be used to configure minimum threshold enforcement for coverage results. Thresholds can be specified as `global`, as a [glob](https://github.com/isaacs/node-glob#glob-primer), and as a directory or file path. If thresholds aren't met, jest will fail. Thresholds specified as a positive number are taken to be the minimum percentage required. Thresholds specified as a negative number represent the maximum number of uncovered entities allowed.
This will be used to configure minimum threshold enforcement for coverage results. Thresholds can be specified as `global`, as a [glob](https://github.com/isaacs/node-glob#glob-primer), and as a directory or file path. If thresholds aren't met, jest will fail.

- If a threshold is set to a **positive** number, it will be interpreted as the **minimum** percentage of coverage required.

- If a threshold is set to a **negative** number, it will be treated as the **maximum** number of uncovered items allowed.

For example, with the following configuration jest will fail if there is less than 80% branch, line, and function coverage, or if there are more than 10 uncovered statements:

Expand All @@ -402,9 +406,13 @@ const {defineConfig} = require('jest');
module.exports = defineConfig({
coverageThreshold: {
global: {
// Requires 80% branch coverage
branches: 80,
// Requires 80% function coverage
functions: 80,
// Requires 80% line coverage
lines: 80,
// Require that no more than 10 statements are uncovered
statements: -10,
},
},
Expand All @@ -417,18 +425,48 @@ import {defineConfig} from 'jest';
export default defineConfig({
coverageThreshold: {
global: {
// Requires 80% branch coverage
branches: 80,
// Requires 80% function coverage
functions: 80,
// Requires 80% line coverage
lines: 80,
// Require that no more than 10 statements are uncovered
statements: -10,
},
},
});
```

If globs or paths are specified alongside `global`, coverage data for matching paths will be subtracted from overall coverage and thresholds will be applied independently. Thresholds for globs are applied to all files matching the glob. If the file specified by path is not found, an error is returned.
#### coverageThreshold.global.lines [number]

Global threshold for lines.

#### coverageThreshold.global.functions [number]

Global threshold for functions.

#### coverageThreshold.global.statements [number]

Global threshold for statements.

#### coverageThreshold.global.branches [number]

Global threshold for branches.

For example, with the following configuration:
#### coverageThreshold[glob-pattern] \[object]

Default: `undefined`

Sets thresholds for files matching the [glob](https://github.com/isaacs/node-glob#glob-primer) pattern. This allows you to enforce a high global standard while also setting specific thresholds for critical files or directories.

:::info

When globs or paths are defined together with a global threshold, Jest applies each threshold independently — specific patterns use their own limits, while the global threshold applies only to files not matched by any pattern.

If the file specified by path is not found, an error is returned.

:::

```js tab title="jest.config.js"
const {defineConfig} = require('jest');
Expand Down Expand Up @@ -488,10 +526,10 @@ export default defineConfig({

Jest will fail if:

- The `./src/components` directory has less than 40% branch or statement coverage.
- The `./src/components` directory has less than 40% branch/statement coverage.
- One of the files matching the `./src/reducers/**/*.js` glob has less than 90% statement coverage.
- The `./src/api/very-important-module.js` file has less than 100% coverage.
- Every remaining file combined has less than 50% coverage (`global`).
- All files that are not matched with `./src/components`, `./src/reducers/**/*.js`, `'./src/api/very-important-module.js'` has less than 50% coverage (`global`).

### `dependencyExtractor` \[string]

Expand Down
51 changes: 32 additions & 19 deletions e2e/__tests__/__snapshots__/coverageThreshold.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,28 +1,41 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`excludes tests matched by path threshold groups from global group 1`] = `
"PASS __tests__/banana.test.js
✓ banana
exports[`exists with 1 if coverage threshold of matched paths is not met independently from global threshold 1`] = `
"Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites."
`;

Jest: "global" coverage threshold for lines (100%) not met: 0%"
exports[`exists with 1 if coverage threshold of matched paths is not met independently from global threshold: stdout 1`] = `
"------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------|---------|----------|---------|---------|-------------------
All files | 75 | 50 | 100 | 75 |
product.js | 75 | 50 | 100 | 75 | 6
------------|---------|----------|---------|---------|-------------------"
`;

exports[`excludes tests matched by path threshold groups from global group 2`] = `
"Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
exports[`exists with 1 if coverage threshold of the rest of non matched paths is not met 1`] = `
"Test Suites: 5 passed, 5 total
Tests: 5 passed, 5 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites."
`;

exports[`excludes tests matched by path threshold groups from global group: stdout 1`] = `
"-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 50 | 100 | 50 | 50 |
apple.js | 0 | 100 | 0 | 0 | 1-2
banana.js | 100 | 100 | 100 | 100 |
-----------|---------|----------|---------|---------|-------------------"
exports[`exists with 1 if coverage threshold of the rest of non matched paths is not met: stdout 1`] = `
"------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------|---------|----------|---------|---------|-------------------
All files | 91.66 | 50 | 100 | 91.66 |
product.js | 75 | 50 | 100 | 75 | 6
sum-01.js | 100 | 100 | 100 | 100 |
sum-02.js | 100 | 100 | 100 | 100 |
sum-03.js | 100 | 100 | 100 | 100 |
sum-04.js | 100 | 100 | 100 | 100 |
------------|---------|----------|---------|---------|-------------------"
`;

exports[`exits with 0 if global threshold group is not found in coverage data: stdout 1`] = `
Expand All @@ -38,7 +51,7 @@ exports[`exits with 1 if coverage threshold is not met 1`] = `
"PASS __tests__/a-banana.js
✓ banana

Jest: "global" coverage threshold for lines (90%) not met: 50%"
Jest: Coverage for lines (50%) does not meet "global" threshold (90%)"
`;

exports[`exits with 1 if coverage threshold is not met 2`] = `
Expand Down Expand Up @@ -86,9 +99,9 @@ exports[`file is matched by all path and glob threshold groups 1`] = `
"PASS __tests__/banana.test.js
✓ banana

Jest: "./" coverage threshold for lines (100%) not met: 50%
Jest: "<<FULL_PATH_TO_BANANA_JS>>" coverage threshold for lines (100%) not met: 50%
Jest: "./banana.js" coverage threshold for lines (100%) not met: 50%"
Jest: Coverage for lines (50%) does not meet "./" threshold (100%)
Jest: Coverage for lines (50%) does not meet "<<FULL_PATH_TO_BANANA_JS>>" threshold (100%)
Jest: Coverage for lines (50%) does not meet "./banana.js" threshold (100%)"
`;

exports[`file is matched by all path and glob threshold groups 2`] = `
Expand Down
147 changes: 129 additions & 18 deletions e2e/__tests__/coverageThreshold.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,49 +129,160 @@ test('exits with 0 if global threshold group is not found in coverage data', ()
expect(stdout).toMatchSnapshot('stdout');
});

test('excludes tests matched by path threshold groups from global group', () => {
test('exists with 1 if coverage threshold of the rest of non matched paths is not met', () => {
const pkgJson = {
jest: {
collectCoverage: true,
collectCoverageFrom: ['**/*.js'],
coverageThreshold: {
'banana.js': {
lines: 100,
'**/*.js': {
branches: 50,
functions: 50,
lines: 50,
statements: 50,
},
global: {
lines: 100,
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
testRegex: '.*\\.test\\.js$',
},
};

writeFiles(DIR, {
'__tests__/banana.test.js': `
const banana = require('../banana.js');
test('banana', () => expect(banana()).toBe(3));
'package.json': JSON.stringify(pkgJson, null, 2),
'product.js': `
function product(a, b) {
// let's simulate a 50% code coverage
if (a > 0) {
return a * b;
} else {
return a * b;
}
}

module.exports = product;
`,
'apple.js': `
module.exports = () => {
return 1 + 2;
};
'product.test.js': `
test('multiplies 2 * 3 to equal 6', () => {
const sum = require('./product');
expect(sum(2, 3)).toBe(6);
});
`,
'banana.js': `
module.exports = () => {
return 1 + 2;
};
'sum-01.js': `
function sum(a, b) {
return a + b;
}

module.exports = sum;
`,
'sum-01.test.js': `
test('adds 1 + 2 to equal 3', () => {
const sum = require('./sum-01');
expect(sum(1, 2)).toBe(3);
});
`,
'sum-02.js': `
function sum(a, b) {
return a + b;
}

module.exports = sum;
`,
'sum-02.test.js': `
test('adds 1 + 2 to equal 3', () => {
const sum = require('./sum-02');
expect(sum(1, 2)).toBe(3);
});
`,
'sum-03.js': `
function sum(a, b) {
return a + b;
}

module.exports = sum;
`,
'sum-03.test.js': `
test('adds 1 + 2 to equal 3', () => {
const sum = require('./sum-03');
expect(sum(1, 2)).toBe(3);
});
`,
'sum-04.js': `
function sum(a, b) {
return a + b;
}

module.exports = sum;
`,
'sum-04.test.js': `
test('adds 1 + 2 to equal 3', () => {
const sum = require('./sum-04');
expect(sum(1, 2)).toBe(3);
});
`,
});

const {stdout, stderr, exitCode} = runJest(
DIR,
['--coverage', '--ci=false'],
{stripAnsi: true},
);
const {summary} = extractSummary(stderr);

expect(exitCode).toBe(1);
expect(summary).toMatchSnapshot();
expect(stdout).toMatchSnapshot('stdout');
});

test('exists with 1 if coverage threshold of matched paths is not met independently from global threshold', () => {
const pkgJson = {
jest: {
collectCoverage: true,
collectCoverageFrom: ['**/*.js'],
coverageThreshold: {
global: {
lines: 70,
},
'product.js': {
lines: 80,
},
},
testRegex: '.*\\.test\\.js$',
},
};
writeFiles(DIR, {
'package.json': JSON.stringify(pkgJson, null, 2),
'product.js': `
function product(a, b) {
// let's simulate a 50% code coverage
if (a > 0) {
return a * b;
} else {
return a * b;
}
}

module.exports = product;
`,
'product.test.js': `
test('multiplies 2 * 3 to equal 6', () => {
const sum = require('./product');
expect(sum(2, 3)).toBe(6);
});
`,
});

const {stdout, stderr, exitCode} = runJest(
DIR,
['--coverage', '--ci=false'],
{stripAnsi: true},
);
const {rest, summary} = extractSummary(stderr);
const {summary} = extractSummary(stderr);

expect(exitCode).toBe(1);
expect(rest).toMatchSnapshot();
expect(summary).toMatchSnapshot();
expect(stdout).toMatchSnapshot('stdout');
});
Expand Down
Loading
Loading