Skip to content

Commit 78266c3

Browse files
authored
feat: add coverage chunks per stylesheet (#4)
* readme improvements * feat: add coverage chunks per stylesheet
1 parent d65c13f commit 78266c3

File tree

3 files changed

+142
-16
lines changed

3 files changed

+142
-16
lines changed

README.md

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,53 @@ npm install @projectwallace/css-code-coverage
2020

2121
## Usage
2222

23-
### Prerequisites
23+
```ts
24+
import { calculate_coverage } from '@projectwallace/css-code-coverage'
25+
26+
function parse_html(html) {
27+
return new DOMParser().parseFromString(html, 'text/html')
28+
}
29+
30+
let report = calculcate_coverage(coverage_data, parse_html)
31+
```
32+
33+
See [src/index.ts](https://github.com/projectwallace/css-code-coverage/blob/main/src/index.ts) for the data that's returned.
34+
35+
## Collecting CSS Coverage
36+
37+
There are two principal ways of collecting CSS Coverage data:
38+
39+
### Browser devtools
40+
41+
In Edge, Chrome or chromium you can manually collect coverage in the browser's DevTools. In all cases you'll generate coverage data manually and the browser will let you export the data to a JSON file. Note that this JSON contains both JS coverage as well as the CSS coverage. Learn how it works:
42+
43+
- Collect coverage in Microsoft Edge: https://learn.microsoft.com/en-us/microsoft-edge/devtools-guide-chromium/coverage/
44+
- Collect coevrage in Google Chrome: https://developer.chrome.com/docs/devtools/coverage/
45+
46+
Additionally, DevTools Tips writes about it in their [explainer](https://devtoolstips.org/tips/en/detect-unused-code/).
47+
48+
### Coverage API
2449

25-
You have collected browser coverage data of your CSS. There are several ways to do this:
50+
Both Puppeteer and Playwright provide an API to programmatically get the coverage data, allowing you to put that directly into this library. Here is the gist:
51+
52+
```ts
53+
// Start collecting coverage
54+
await page.coverage.startCSSCoverage()
55+
// Load the page, do all sorts of interactions to increase coverage, etc.
56+
await page.goto('http://example.com')
57+
// Stop the coverage and store the result in a variable to pass along
58+
let coverage = await page.coverage.stopCSSCoverage()
59+
60+
// Now we can process it
61+
import { calculate_coverage } from '@projectwallace/css-code-coverage'
62+
63+
function parse_html(html) {
64+
return new DOMParser().parseFromString(html, 'text/html')
65+
}
66+
67+
let report = calculcate_coverage(coverage, parse_html)
68+
```
2669

27-
1. in the browser devtools in [Edge](https://learn.microsoft.com/en-us/microsoft-edge/devtools-guide-chromium/coverage/)/[Chrome](https://developer.chrome.com/docs/devtools/coverage/)/chromium
2870
1. Via the `coverage.startCSSCoverage()` API that headless browsers like [Playwright](https://playwright.dev/docs/api/class-coverage#coverage-start-css-coverage) or [Puppeteer](https://pptr.dev/api/puppeteer.coverage.startcsscoverage/) provide.
2971

3072
Either way you end up with one or more JSON files that contain coverage data.
@@ -50,27 +92,17 @@ for (let file of files) {
5092
}
5193
```
5294

53-
### Bringing it together
54-
55-
```ts
56-
import { calculate_coverage } from '@projectwallace/css-code-coverage'
57-
58-
let report = calculcate_coverage(coverage_data, parse_html)
59-
```
60-
61-
See [src/index.ts](https://github.com/projectwallace/css-code-coverage/blob/main/src/index.ts) for the data that's returned.
62-
6395
### Optional: coverage from `<style>` blocks
6496

65-
Covergae generators also create coverage ranges for `<style>` blocks in HTML. If this applies to your code you should provide a HTML parser that we use to 'scrape' the HTML in case the browser gives us not just plain CSS contents. Depending on where you run this analysis you can use:
97+
Coverage generators also create coverage ranges for `<style>` blocks in HTML. If this applies to your code you should provide a HTML parser that we use to 'scrape' the HTML in case the browser gives us not just plain CSS contents. Depending on where you run this analysis you can use:
6698

6799
1. Browser:
68100
```ts
69101
function parse_html(html) {
70102
return new DOMParser().parseFromString(html, 'text/html')
71103
}
72104
```
73-
1. Node (using [linkedom](https://github.com/WebReflection/linkedom) in this example):
105+
1. Node (using [linkedom](https://github.com/WebReflection/linkedom) in this example, but other parsers could work, too):
74106

75107
```ts
76108
// $ npm install linkedom

src/index.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,64 @@ test.describe('from coverage data downloaded directly from the browser as JSON',
195195
]),
196196
)
197197
})
198+
199+
test('calculates chunks', () => {
200+
let result = calculate_coverage(coverage, html_parser)
201+
expect(result.coverage_per_stylesheet.at(0)?.chunks).toEqual([
202+
{ start_line: 1, is_covered: true, end_line: 4, total_lines: 4 },
203+
{ start_line: 4, is_covered: false, end_line: 8, total_lines: 5 },
204+
{ start_line: 8, is_covered: true, end_line: 10, total_lines: 3 },
205+
{ start_line: 10, is_covered: false, end_line: 11, total_lines: 2 },
206+
{ start_line: 11, is_covered: true, end_line: 14, total_lines: 4 },
207+
])
208+
})
209+
210+
test('calculates chunks for fully covered file', () => {
211+
let result = calculate_coverage(
212+
[
213+
{
214+
url: 'https://example.com',
215+
ranges: [
216+
{
217+
start: 0,
218+
end: 19,
219+
},
220+
],
221+
text: 'h1 { color: blue; }',
222+
},
223+
],
224+
html_parser,
225+
)
226+
expect(result.coverage_per_stylesheet.at(0)?.chunks).toEqual([
227+
{
228+
start_line: 1,
229+
is_covered: true,
230+
end_line: 3,
231+
total_lines: 3,
232+
},
233+
])
234+
})
235+
236+
test('calculates chunks for fully uncovered file', () => {
237+
let result = calculate_coverage(
238+
[
239+
{
240+
url: 'https://example.com',
241+
ranges: [],
242+
text: 'h1 { color: blue; }',
243+
},
244+
],
245+
html_parser,
246+
)
247+
expect(result.coverage_per_stylesheet.at(0)?.chunks).toEqual([
248+
{
249+
start_line: 1,
250+
is_covered: false,
251+
end_line: 3,
252+
total_lines: 3,
253+
},
254+
])
255+
})
198256
})
199257

200258
test('handles empty input', () => {

src/index.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ export type StylesheetCoverage = CoverageData & {
2020
text: string
2121
ranges: Range[]
2222
line_coverage: Uint8Array
23+
chunks: {
24+
is_covered: boolean
25+
start_line: number
26+
end_line: number
27+
total_lines: number
28+
}[]
2329
}
2430

2531
export type CoverageResult = CoverageData & {
@@ -117,6 +123,36 @@ export function calculate_coverage(coverage: Coverage[], parse_html?: Parser): C
117123
offset = next_offset
118124
}
119125

126+
// Create "chunks" of covered/uncovered lines for easier rendering later on
127+
let chunks = [
128+
{
129+
start_line: 1,
130+
is_covered: line_coverage[0] === 1,
131+
end_line: 0,
132+
total_lines: 0,
133+
},
134+
]
135+
136+
for (let index = 0; index < line_coverage.length; index++) {
137+
let is_covered = line_coverage[index]
138+
if (index > 0 && is_covered !== line_coverage[index - 1]) {
139+
let last_chunk = chunks.at(-1)!
140+
last_chunk.end_line = index
141+
last_chunk.total_lines = index - last_chunk.start_line + 1
142+
143+
chunks.push({
144+
start_line: index,
145+
is_covered: is_covered === 1,
146+
end_line: index,
147+
total_lines: 0,
148+
})
149+
}
150+
}
151+
152+
let last_chunk = chunks.at(-1)!
153+
last_chunk.total_lines = line_coverage.length - last_chunk.start_line + 1
154+
last_chunk.end_line = line_coverage.length
155+
120156
return {
121157
url,
122158
text,
@@ -130,7 +166,7 @@ export function calculate_coverage(coverage: Coverage[], parse_html?: Parser): C
130166
total_lines: total_file_lines,
131167
covered_lines: file_lines_covered,
132168
uncovered_lines: total_file_lines - file_lines_covered,
133-
// TODO: { is_covered: boolean, start_offset: number, start_line: number, end_offset: number, end_line: number }[]
169+
chunks,
134170
}
135171
})
136172

0 commit comments

Comments
 (0)