Skip to content

Commit cbeb294

Browse files
committed
fix(jest-reporters): apply global coverage threshold to unmatched pattern files
1 parent 580d4b7 commit cbeb294

File tree

6 files changed

+292
-140
lines changed

6 files changed

+292
-140
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- `[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))
1111
- `[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))
1212
- `[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))
13+
- `[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))
1314

1415
### Chore & Maintenance
1516

docs/Configuration.md

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,11 @@ For more information about the options object shape refer to `CoverageReporterWi
392392

393393
Default: `undefined`
394394

395-
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.
395+
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.
396+
397+
- If a threshold is set to a **positive** number, it will be interpreted as the **minimum** percentage of coverage required.
398+
399+
- If a threshold is set to a **negative** number, it will be treated as the **maximum** number of uncovered items allowed.
396400

397401
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:
398402

@@ -402,9 +406,13 @@ const {defineConfig} = require('jest');
402406
module.exports = defineConfig({
403407
coverageThreshold: {
404408
global: {
409+
// Requires 80% branch coverage
405410
branches: 80,
411+
// Requires 80% function coverage
406412
functions: 80,
413+
// Requires 80% line coverage
407414
lines: 80,
415+
// Require that no more than 10 statements are uncovered
408416
statements: -10,
409417
},
410418
},
@@ -417,18 +425,48 @@ import {defineConfig} from 'jest';
417425
export default defineConfig({
418426
coverageThreshold: {
419427
global: {
428+
// Requires 80% branch coverage
420429
branches: 80,
430+
// Requires 80% function coverage
421431
functions: 80,
432+
// Requires 80% line coverage
422433
lines: 80,
434+
// Require that no more than 10 statements are uncovered
423435
statements: -10,
424436
},
425437
},
426438
});
427439
```
428440

429-
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.
441+
#### coverageThreshold.global.lines [number]
442+
443+
Global threshold for lines.
444+
445+
#### coverageThreshold.global.functions [number]
446+
447+
Global threshold for functions.
448+
449+
#### coverageThreshold.global.statements [number]
450+
451+
Global threshold for statements.
452+
453+
#### coverageThreshold.global.branches [number]
454+
455+
Global threshold for branches.
430456

431-
For example, with the following configuration:
457+
#### coverageThreshold[glob-pattern] \[object]
458+
459+
Default: `undefined`
460+
461+
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.
462+
463+
:::info
464+
465+
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.
466+
467+
If the file specified by path is not found, an error is returned.
468+
469+
:::
432470

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

489527
Jest will fail if:
490528

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

496534
### `dependencyExtractor` \[string]
497535

e2e/__tests__/__snapshots__/coverageThreshold.test.ts.snap

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,41 @@
11
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
22

3-
exports[`excludes tests matched by path threshold groups from global group 1`] = `
4-
"PASS __tests__/banana.test.js
5-
✓ banana
3+
exports[`exists with 1 if coverage threshold of matched paths is not met independently from global threshold 1`] = `
4+
"Test Suites: 1 passed, 1 total
5+
Tests: 1 passed, 1 total
6+
Snapshots: 0 total
7+
Time: <<REPLACED>>
8+
Ran all test suites."
9+
`;
610
7-
Jest: "global" coverage threshold for lines (100%) not met: 0%"
11+
exports[`exists with 1 if coverage threshold of matched paths is not met independently from global threshold: stdout 1`] = `
12+
"------------|---------|----------|---------|---------|-------------------
13+
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
14+
------------|---------|----------|---------|---------|-------------------
15+
All files | 75 | 50 | 100 | 75 |
16+
product.js | 75 | 50 | 100 | 75 | 6
17+
------------|---------|----------|---------|---------|-------------------"
818
`;
919
10-
exports[`excludes tests matched by path threshold groups from global group 2`] = `
11-
"Test Suites: 1 passed, 1 total
12-
Tests: 1 passed, 1 total
20+
exports[`exists with 1 if coverage threshold of the rest of non matched paths is not met 1`] = `
21+
"Test Suites: 5 passed, 5 total
22+
Tests: 5 passed, 5 total
1323
Snapshots: 0 total
1424
Time: <<REPLACED>>
1525
Ran all test suites."
1626
`;
1727
18-
exports[`excludes tests matched by path threshold groups from global group: stdout 1`] = `
19-
"-----------|---------|----------|---------|---------|-------------------
20-
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
21-
-----------|---------|----------|---------|---------|-------------------
22-
All files | 50 | 100 | 50 | 50 |
23-
apple.js | 0 | 100 | 0 | 0 | 1-2
24-
banana.js | 100 | 100 | 100 | 100 |
25-
-----------|---------|----------|---------|---------|-------------------"
28+
exports[`exists with 1 if coverage threshold of the rest of non matched paths is not met: stdout 1`] = `
29+
"------------|---------|----------|---------|---------|-------------------
30+
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
31+
------------|---------|----------|---------|---------|-------------------
32+
All files | 91.66 | 50 | 100 | 91.66 |
33+
product.js | 75 | 50 | 100 | 75 | 6
34+
sum-01.js | 100 | 100 | 100 | 100 |
35+
sum-02.js | 100 | 100 | 100 | 100 |
36+
sum-03.js | 100 | 100 | 100 | 100 |
37+
sum-04.js | 100 | 100 | 100 | 100 |
38+
------------|---------|----------|---------|---------|-------------------"
2639
`;
2740
2841
exports[`exits with 0 if global threshold group is not found in coverage data: stdout 1`] = `
@@ -38,7 +51,7 @@ exports[`exits with 1 if coverage threshold is not met 1`] = `
3851
"PASS __tests__/a-banana.js
3952
✓ banana
4053
41-
Jest: "global" coverage threshold for lines (90%) not met: 50%"
54+
Jest: Coverage for lines (50%) does not meet "global" threshold (90%)"
4255
`;
4356
4457
exports[`exits with 1 if coverage threshold is not met 2`] = `
@@ -86,9 +99,9 @@ exports[`file is matched by all path and glob threshold groups 1`] = `
8699
"PASS __tests__/banana.test.js
87100
✓ banana
88101
89-
Jest: "./" coverage threshold for lines (100%) not met: 50%
90-
Jest: "<<FULL_PATH_TO_BANANA_JS>>" coverage threshold for lines (100%) not met: 50%
91-
Jest: "./banana.js" coverage threshold for lines (100%) not met: 50%"
102+
Jest: Coverage for lines (50%) does not meet "./" threshold (100%)
103+
Jest: Coverage for lines (50%) does not meet "<<FULL_PATH_TO_BANANA_JS>>" threshold (100%)
104+
Jest: Coverage for lines (50%) does not meet "./banana.js" threshold (100%)"
92105
`;
93106
94107
exports[`file is matched by all path and glob threshold groups 2`] = `

e2e/__tests__/coverageThreshold.test.ts

Lines changed: 129 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -129,49 +129,160 @@ test('exits with 0 if global threshold group is not found in coverage data', ()
129129
expect(stdout).toMatchSnapshot('stdout');
130130
});
131131

132-
test('excludes tests matched by path threshold groups from global group', () => {
132+
test('exists with 1 if coverage threshold of the rest of non matched paths is not met', () => {
133133
const pkgJson = {
134134
jest: {
135135
collectCoverage: true,
136136
collectCoverageFrom: ['**/*.js'],
137137
coverageThreshold: {
138-
'banana.js': {
139-
lines: 100,
138+
'**/*.js': {
139+
branches: 50,
140+
functions: 50,
141+
lines: 50,
142+
statements: 50,
140143
},
141144
global: {
142-
lines: 100,
145+
branches: 80,
146+
functions: 80,
147+
lines: 80,
148+
statements: 80,
143149
},
144150
},
151+
testRegex: '.*\\.test\\.js$',
145152
},
146153
};
147-
148154
writeFiles(DIR, {
149-
'__tests__/banana.test.js': `
150-
const banana = require('../banana.js');
151-
test('banana', () => expect(banana()).toBe(3));
155+
'package.json': JSON.stringify(pkgJson, null, 2),
156+
'product.js': `
157+
function product(a, b) {
158+
// let's simulate a 50% code coverage
159+
if (a > 0) {
160+
return a * b;
161+
} else {
162+
return a * b;
163+
}
164+
}
165+
166+
module.exports = product;
152167
`,
153-
'apple.js': `
154-
module.exports = () => {
155-
return 1 + 2;
156-
};
168+
'product.test.js': `
169+
test('multiplies 2 * 3 to equal 6', () => {
170+
const sum = require('./product');
171+
expect(sum(2, 3)).toBe(6);
172+
});
157173
`,
158-
'banana.js': `
159-
module.exports = () => {
160-
return 1 + 2;
161-
};
174+
'sum-01.js': `
175+
function sum(a, b) {
176+
return a + b;
177+
}
178+
179+
module.exports = sum;
180+
`,
181+
'sum-01.test.js': `
182+
test('adds 1 + 2 to equal 3', () => {
183+
const sum = require('./sum-01');
184+
expect(sum(1, 2)).toBe(3);
185+
});
186+
`,
187+
'sum-02.js': `
188+
function sum(a, b) {
189+
return a + b;
190+
}
191+
192+
module.exports = sum;
193+
`,
194+
'sum-02.test.js': `
195+
test('adds 1 + 2 to equal 3', () => {
196+
const sum = require('./sum-02');
197+
expect(sum(1, 2)).toBe(3);
198+
});
199+
`,
200+
'sum-03.js': `
201+
function sum(a, b) {
202+
return a + b;
203+
}
204+
205+
module.exports = sum;
206+
`,
207+
'sum-03.test.js': `
208+
test('adds 1 + 2 to equal 3', () => {
209+
const sum = require('./sum-03');
210+
expect(sum(1, 2)).toBe(3);
211+
});
212+
`,
213+
'sum-04.js': `
214+
function sum(a, b) {
215+
return a + b;
216+
}
217+
218+
module.exports = sum;
162219
`,
220+
'sum-04.test.js': `
221+
test('adds 1 + 2 to equal 3', () => {
222+
const sum = require('./sum-04');
223+
expect(sum(1, 2)).toBe(3);
224+
});
225+
`,
226+
});
227+
228+
const {stdout, stderr, exitCode} = runJest(
229+
DIR,
230+
['--coverage', '--ci=false'],
231+
{stripAnsi: true},
232+
);
233+
const {summary} = extractSummary(stderr);
234+
235+
expect(exitCode).toBe(1);
236+
expect(summary).toMatchSnapshot();
237+
expect(stdout).toMatchSnapshot('stdout');
238+
});
239+
240+
test('exists with 1 if coverage threshold of matched paths is not met independently from global threshold', () => {
241+
const pkgJson = {
242+
jest: {
243+
collectCoverage: true,
244+
collectCoverageFrom: ['**/*.js'],
245+
coverageThreshold: {
246+
global: {
247+
lines: 70,
248+
},
249+
'product.js': {
250+
lines: 80,
251+
},
252+
},
253+
testRegex: '.*\\.test\\.js$',
254+
},
255+
};
256+
writeFiles(DIR, {
163257
'package.json': JSON.stringify(pkgJson, null, 2),
258+
'product.js': `
259+
function product(a, b) {
260+
// let's simulate a 50% code coverage
261+
if (a > 0) {
262+
return a * b;
263+
} else {
264+
return a * b;
265+
}
266+
}
267+
268+
module.exports = product;
269+
`,
270+
'product.test.js': `
271+
test('multiplies 2 * 3 to equal 6', () => {
272+
const sum = require('./product');
273+
expect(sum(2, 3)).toBe(6);
274+
});
275+
`,
164276
});
165277

166278
const {stdout, stderr, exitCode} = runJest(
167279
DIR,
168280
['--coverage', '--ci=false'],
169281
{stripAnsi: true},
170282
);
171-
const {rest, summary} = extractSummary(stderr);
283+
const {summary} = extractSummary(stderr);
172284

173285
expect(exitCode).toBe(1);
174-
expect(rest).toMatchSnapshot();
175286
expect(summary).toMatchSnapshot();
176287
expect(stdout).toMatchSnapshot('stdout');
177288
});

0 commit comments

Comments
 (0)