Skip to content

Commit 54887ed

Browse files
authored
Coverage formatter fix (#635)
1 parent 780e533 commit 54887ed

16 files changed

+726
-706
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Bug-fixes within the same version aren't needed
66
## Master
77
88
* use assertion's fullName in TestResult instead of the source test name - @connectdotz
9+
* consolidated and simplified coverage formatter parsing logic; added overlay color customization; change coverage formatter/colors will take effect without restarting vscode; updated coverage help in README. - @connectdotz
910
-->
1011

1112
### 4.0.0-alpha.1

README.md

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ Content
1515
- [How to start Jest?](#how-to-start-jest)
1616
- [How do I debug tests?](#how-do-i-debug-tests)
1717
- [Notes for troubleshooting](#notes-for-troubleshooting)
18-
- [How do I show code coverage?](#how-do-i-show-code-coverage)
18+
- [Coverage](#coverage)
19+
- [How do I show code coverage?](#how-do-i-show-code-coverage)
20+
- [How to customize coverage overlay](#how-to-customize-coverage-overlay)
21+
- [Understand the coverage overlay](#understand-the-coverage-overlay)
1922
- [Inspiration](#inspiration)
20-
- [Wanted](#wanted)
2123
- [Troubleshooting](#troubleshooting)
2224
- [start jest from non-root folder](#start-jest-from-non-root-folder)
2325
- [use extension in multiroot environment](#use-extension-in-multiroot-environment)
@@ -93,7 +95,8 @@ Starting with debugging is possible by clicking on the `debug` CodeLense above a
9395
In contrast to previous versions of this plugin the debug settings are now independent from VS Code's `jest.pathToJest` and `jest.pathToConfig` setting. If you had to modify one of these, you pretty surely have to create a custom debug configuration and modify its path. This especially includes cases, in which `jest` isn't at its default location.
9496

9597

96-
## How do I show code coverage?
98+
## Coverage
99+
### How do I show code coverage?
97100

98101
Starting from [v3.1](https://github.com/jest-community/vscode-jest/releases/tag/v3.1.0), code coverage can be easily turned on/off at runtime without customization.
99102

@@ -105,29 +108,48 @@ The coverage mode, along with watch mode, are shown in StatusBar:
105108

106109
_(The initial coverage mode is `off` but can be changed by adding `"jest.showCoverageOnLoad": true` in settings.)_
107110

108-
<!--
109-
### TODO: Change overlay format
110-
Use a setting that's one of "", "", etc.
111-
```json
112-
{
113-
"jest.?": ""
114-
}
115-
```
116111

117-
Screenshots:
118-
* Default
119-
* Gutters
120-
-->
112+
### How to customize coverage overlay
113+
Coverage overlay determines how the coverage info is shown to users. This extension provides 2 customization points:
114+
1. coverage style via `jest.coverageFormatter`
115+
2. the coverage color scheme via `jest.coverageColors`.
121116

122-
## Inspiration
117+
**Coverage Style**
118+
Use `jest.coverageFormatter` to choose from the following, for example `"jest.coverageFormatter": "GutterFormatter"`.
123119

124-
I'd like to give a shout out to [Wallaby.js](https://wallabyjs.com), which is a significantly more comprehensive and covers a lot more editors, if this extension interests you - check out that too.
120+
- **DefaultFormatter**: high light uncovered and partially-covered code inlilne as well as on the right overview ruler. (this is the default)
121+
![coverage-DefaultFormatter.png](./images/coverage-DefaultFormatter.png)
122+
- **GutterFormatter**: render coverage status in the gutter as well as the overview ruler.
125123

124+
![coverage-GutterFormatter.png](./images/coverage-GutterFormatter.png)
126125

127-
## Wanted
126+
_(Note, there is an known issue in vscode (microsoft/vscode#5923) that gutter decorators could interfere with debug breakpoints visibility. Therefore, you probably want to disable coverage before debugging or switch to DefaultFormatter)_
128127

129-
Someone to take responsibility for ensuring that the default setup for create-react-app is always working. All the current authors use TypeScript and React/React Native and so have very little familiarity with changes to CRA. _Apply via PRs :D_.
128+
**Coverage Colors**
129+
Besides the formatter, user can also customize the color via `jest.coverageColors` to change color for 3 coverage categories: `"uncovered", "covered", or "partially-covered"`, for example:
130+
```
131+
"jest.coverageColors": {
132+
"uncovered": "rgba(255,99,71, 0.2)",
133+
"partially-covered": "rgba(255,215,0, 0.2)",
134+
}
135+
```
136+
the default color scheme below, note the opacity might differ per formatter:
137+
```
138+
"jest.coverageColors": {
139+
"covered": "rgba(9, 156, 65, 0.4)",
140+
"uncovered": "rgba(121, 31, 10, 0.4)",
141+
"partially-covered": "rgba(235, 198, 52, 0.4)",
142+
}
143+
```
144+
### Understand the coverage overlay
145+
Depends on the formatter you choose, there are 3 types of coverage you might see in your source code, distinguished by colors:
146+
- "covered": if the code is covered. Marked as <span style="color:green">"green"</span> by default.
147+
- "not-covered": if the code is not covered. Marked as <span style="color:red">"red"</span> by default.
148+
- "partially-covered": Usually this mean the branch (such as if, switch statements) only partially tested. Marked as <span style="color:yellow">"yellow"</span> by default.
149+
- _Please note, istanbuljs (the library jest used to generate coverage info) reports switch branch coverage with the first "case" statement instead of the "switch" statement._
150+
## Inspiration
130151

152+
I'd like to give a shout out to [Wallaby.js](https://wallabyjs.com), which is a significantly more comprehensive and covers a lot more editors, if this extension interests you - check out that too.
131153

132154
## Troubleshooting
133155

264 KB
Loading
260 KB
Loading

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@
123123
"default": "DefaultFormatter",
124124
"scope": "resource"
125125
},
126+
"jest.coverageColors": {
127+
"description": "Coverage indicator color override",
128+
"type": "object",
129+
"scope": "resource"
130+
},
126131
"jest.enableCodeLens": {
127132
"description": "Whether codelens for debugging should show",
128133
"type": "boolean",

src/Coverage/CoverageOverlay.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import { GutterFormatter } from './Formatters/GutterFormatter';
55
import * as vscode from 'vscode';
66
import { hasDocument } from '../editor';
77

8+
export type CoverageStatus = 'covered' | 'partially-covered' | 'uncovered';
9+
export type CoverageColors = {
10+
[key in CoverageStatus]?: string;
11+
};
12+
813
export class CoverageOverlay {
914
static readonly defaultVisibility = false;
1015
static readonly defaultFormatter = 'DefaultFormatter';
@@ -15,21 +20,22 @@ export class CoverageOverlay {
1520
context: vscode.ExtensionContext,
1621
coverageMapProvider: CoverageMapProvider,
1722
enabled: boolean = CoverageOverlay.defaultVisibility,
18-
coverageFormatter: string = CoverageOverlay.defaultFormatter
23+
coverageFormatter: string = CoverageOverlay.defaultFormatter,
24+
colors?: CoverageColors
1925
) {
2026
this._enabled = enabled;
2127
switch (coverageFormatter) {
2228
case 'GutterFormatter':
23-
this.formatter = new GutterFormatter(context, coverageMapProvider);
29+
this.formatter = new GutterFormatter(context, coverageMapProvider, colors);
2430
break;
2531

2632
default:
27-
this.formatter = new DefaultFormatter(coverageMapProvider);
33+
this.formatter = new DefaultFormatter(coverageMapProvider, colors);
2834
break;
2935
}
3036
}
3137

32-
get enabled() {
38+
get enabled(): boolean {
3339
return this._enabled;
3440
}
3541

@@ -38,18 +44,23 @@ export class CoverageOverlay {
3844
this.updateVisibleEditors();
3945
}
4046

41-
toggleVisibility() {
47+
/** give formatter opportunity to dispose the decorators */
48+
dispose(): void {
49+
this.formatter.dispose();
50+
}
51+
52+
toggleVisibility(): void {
4253
this._enabled = !this._enabled;
4354
this.updateVisibleEditors();
4455
}
4556

46-
updateVisibleEditors() {
57+
updateVisibleEditors(): void {
4758
for (const editor of vscode.window.visibleTextEditors) {
4859
this.update(editor);
4960
}
5061
}
5162

52-
update(editor: vscode.TextEditor) {
63+
update(editor: vscode.TextEditor): void {
5364
if (!hasDocument(editor)) {
5465
return;
5566
}
Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,113 @@
11
import { CoverageMapProvider } from '../CoverageMapProvider';
22
import * as vscode from 'vscode';
3+
import { CoverageColors, CoverageStatus } from '../CoverageOverlay';
4+
import { FileCoverage } from 'istanbul-lib-coverage';
35

6+
export type CoverageRanges = {
7+
[status in CoverageStatus]?: vscode.Range[];
8+
};
9+
10+
type FunctionCoverageByLine = { [line: number]: number };
411
export abstract class AbstractFormatter {
512
protected coverageMapProvider: CoverageMapProvider;
13+
protected colors?: CoverageColors;
614

7-
constructor(coverageMapProvider: CoverageMapProvider) {
15+
constructor(coverageMapProvider: CoverageMapProvider, colors?: CoverageColors) {
816
this.coverageMapProvider = coverageMapProvider;
17+
this.colors = colors;
918
}
1019

1120
abstract format(editor: vscode.TextEditor);
21+
/** remove decoractors for the given editor */
1222
abstract clear(editor: vscode.TextEditor);
23+
/** dispose decoractors for all editors */
24+
abstract dispose();
25+
26+
/**
27+
* returns rgba color string similar to istanbul html report color scheme
28+
* @param status
29+
* @param opacity
30+
*/
31+
getColorString(status: CoverageStatus, opacity: number): string {
32+
if (opacity > 1 || opacity < 0) {
33+
throw new Error(`invalid opacity (${opacity}): value is not between 0 - 1`);
34+
}
35+
36+
switch (status) {
37+
case 'covered':
38+
return this.colors?.[status] ?? `rgba(9, 156, 65, ${opacity})`; // green
39+
case 'partially-covered':
40+
return this.colors?.[status] ?? `rgba(235, 198, 52, ${opacity})`; // yellow
41+
case 'uncovered':
42+
return this.colors?.[status] ?? `rgba(121, 31, 10, ${opacity})`; // red
43+
default:
44+
throw new Error(`unrecognized status: ${status}`);
45+
}
46+
}
47+
48+
private getFunctionCoverageByLine(fileCoverage: FileCoverage): FunctionCoverageByLine {
49+
const lineCoverage: FunctionCoverageByLine = {};
50+
Object.entries(fileCoverage.fnMap).forEach(([k, { decl }]) => {
51+
const hits = fileCoverage.f[k];
52+
for (let idx = decl.start.line; idx <= decl.end.line; idx++) {
53+
lineCoverage[idx] = hits;
54+
}
55+
});
56+
return lineCoverage;
57+
}
58+
/**
59+
* mapping the coverage map to a line-based coverage ranges
60+
* @param editor
61+
*/
62+
lineCoverageRanges(
63+
editor: vscode.TextEditor,
64+
onNoCoverageInfo?: () => CoverageStatus
65+
): CoverageRanges {
66+
const ranges: CoverageRanges = {};
67+
const fileCoverage = this.coverageMapProvider.getFileCoverage(editor.document.fileName);
68+
if (!fileCoverage) {
69+
return ranges;
70+
}
71+
const lineCoverage = fileCoverage.getLineCoverage();
72+
const branchCoveravge = fileCoverage.getBranchCoverageByLine();
73+
const funcCoverage = this.getFunctionCoverageByLine(fileCoverage);
74+
75+
// consolidate the coverage by line
76+
for (let line = 1; line <= editor.document.lineCount; line++) {
77+
const zeroBasedLineNumber = line - 1;
78+
const lc = lineCoverage[line];
79+
const bc = branchCoveravge[line];
80+
const fc = funcCoverage[line];
81+
let status: CoverageStatus;
82+
if (fc != null) {
83+
status = fc > 0 ? 'covered' : 'uncovered';
84+
} else if (bc != null) {
85+
switch (bc.coverage) {
86+
case 100:
87+
status = 'covered';
88+
break;
89+
case 0:
90+
status = 'uncovered';
91+
break;
92+
default:
93+
status = 'partially-covered';
94+
break;
95+
}
96+
} else if (lc != null) {
97+
status = lc > 0 ? 'covered' : 'uncovered';
98+
} else if (onNoCoverageInfo) {
99+
status = onNoCoverageInfo();
100+
} else {
101+
continue;
102+
}
103+
104+
const range = new vscode.Range(zeroBasedLineNumber, 0, zeroBasedLineNumber, 0);
105+
if (ranges[status] != null) {
106+
ranges[status].push(range);
107+
} else {
108+
ranges[status] = [range];
109+
}
110+
}
111+
return ranges;
112+
}
13113
}
Lines changed: 29 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,40 @@
11
import { AbstractFormatter } from './AbstractFormatter';
22
import * as vscode from 'vscode';
3-
import { FileCoverage } from 'istanbul-lib-coverage';
4-
import { isValidLocation } from './helpers';
5-
6-
const uncoveredBranch = vscode.window.createTextEditorDecorationType({
7-
backgroundColor: 'rgba(216,134,123,0.4)',
8-
overviewRulerColor: 'rgba(216,134,123,0.8)',
9-
overviewRulerLane: vscode.OverviewRulerLane.Left,
10-
});
11-
12-
const uncoveredLine = vscode.window.createTextEditorDecorationType({
13-
isWholeLine: true,
14-
backgroundColor: 'rgba(216,134,123,0.4)',
15-
overviewRulerColor: 'rgba(216,134,123,0.8)',
16-
overviewRulerLane: vscode.OverviewRulerLane.Left,
17-
});
3+
import { CoverageMapProvider } from '../CoverageMapProvider';
4+
import { CoverageColors } from '../CoverageOverlay';
185

196
export class DefaultFormatter extends AbstractFormatter {
20-
format(editor: vscode.TextEditor) {
21-
const fileCoverage = this.coverageMapProvider.getFileCoverage(editor.document.fileName);
22-
if (!fileCoverage) {
23-
return;
24-
}
25-
26-
this.formatBranches(editor, fileCoverage);
27-
this.formatUncoveredLines(editor, fileCoverage);
28-
}
29-
30-
formatBranches(editor: vscode.TextEditor, fileCoverage: FileCoverage) {
31-
const ranges = [];
32-
33-
Object.keys(fileCoverage.b).forEach((branchIndex) => {
34-
fileCoverage.b[branchIndex].forEach((hitCount, locationIndex) => {
35-
if (hitCount > 0) {
36-
return;
37-
}
38-
39-
const branch = fileCoverage.branchMap[branchIndex].locations[locationIndex];
40-
if (!isValidLocation(branch)) {
41-
return;
42-
}
43-
44-
// If the value is `null`, then set it to the first character on its
45-
// line.
46-
const endColumn = branch.end.column || 0;
47-
48-
ranges.push(
49-
new vscode.Range(
50-
branch.start.line - 1,
51-
branch.start.column,
52-
branch.end.line - 1,
53-
endColumn
54-
)
55-
);
56-
});
7+
readonly uncoveredLine: vscode.TextEditorDecorationType;
8+
readonly partiallyCoveredLine: vscode.TextEditorDecorationType;
9+
10+
constructor(coverageMapProvider: CoverageMapProvider, colors?: CoverageColors) {
11+
super(coverageMapProvider, colors);
12+
this.partiallyCoveredLine = vscode.window.createTextEditorDecorationType({
13+
isWholeLine: true,
14+
backgroundColor: this.getColorString('partially-covered', 0.4),
15+
overviewRulerColor: this.getColorString('partially-covered', 0.8),
16+
overviewRulerLane: vscode.OverviewRulerLane.Left,
17+
});
18+
this.uncoveredLine = vscode.window.createTextEditorDecorationType({
19+
isWholeLine: true,
20+
backgroundColor: this.getColorString('uncovered', 0.4),
21+
overviewRulerColor: this.getColorString('uncovered', 0.8),
22+
overviewRulerLane: vscode.OverviewRulerLane.Left,
5723
});
58-
59-
editor.setDecorations(uncoveredBranch, ranges);
6024
}
6125

62-
formatUncoveredLines(editor: vscode.TextEditor, fileCoverage: FileCoverage) {
63-
const lines = fileCoverage.getUncoveredLines();
64-
65-
const ranges = [];
66-
for (const oneBasedLineNumber of lines) {
67-
const zeroBasedLineNumber = Number(oneBasedLineNumber) - 1;
68-
ranges.push(new vscode.Range(zeroBasedLineNumber, 0, zeroBasedLineNumber, 0));
69-
}
70-
71-
editor.setDecorations(uncoveredLine, ranges);
26+
format(editor: vscode.TextEditor): void {
27+
const coverageRanges = this.lineCoverageRanges(editor);
28+
editor.setDecorations(this.uncoveredLine, coverageRanges['uncovered'] ?? []);
29+
editor.setDecorations(this.partiallyCoveredLine, coverageRanges['partially-covered'] ?? []);
7230
}
7331

74-
clear(editor: vscode.TextEditor) {
75-
editor.setDecorations(uncoveredLine, []);
76-
editor.setDecorations(uncoveredBranch, []);
32+
clear(editor: vscode.TextEditor): void {
33+
editor.setDecorations(this.uncoveredLine, []);
34+
editor.setDecorations(this.partiallyCoveredLine, []);
35+
}
36+
dispose(): void {
37+
this.partiallyCoveredLine.dispose();
38+
this.uncoveredLine.dispose();
7739
}
7840
}

0 commit comments

Comments
 (0)