Skip to content
Merged
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
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
"knip": "knip"
},
"dependencies": {
"@projectwallace/format-css": "^2.2.0"
"@projectwallace/css-parser": "^0.13.8",
"@projectwallace/format-css": "^2.2.6"
},
"devDependencies": {
"@codecov/vite-plugin": "^1.9.1",
Expand All @@ -56,6 +57,9 @@
"tsdown": "^0.21.2",
"typescript": "^5.9.3"
},
"overrides": {
"@projectwallace/css-parser": "$@projectwallace/css-parser"
},
"engines": {
"node": ">=20"
},
Expand Down
51 changes: 51 additions & 0 deletions src/lib/chunkify.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { tokenize } from '@projectwallace/css-parser/tokenizer'
import type { Coverage } from './parse-coverage'

type Chunk = {
Expand Down Expand Up @@ -51,6 +52,56 @@ function merge(stylesheet: ChunkedCoverage): ChunkedCoverage {
}
}

export function mark_comments_as_covered(stylesheet: ChunkedCoverage): ChunkedCoverage {
let new_chunks: Chunk[] = []

for (let chunk of stylesheet.chunks) {
if (chunk.is_covered) {
new_chunks.push(chunk)
continue
}

let text = stylesheet.text.slice(chunk.start_offset, chunk.end_offset)
let comments: Array<{ start: number; end: number }> = []

for (const _ of tokenize(text, ({ start, end }) => comments.push({ start, end }))) {
// consume the generator to drive the on_comment callback
}

if (comments.length === 0) {
new_chunks.push(chunk)
continue
}

let last_end = 0
for (let comment of comments) {
if (comment.start > last_end) {
new_chunks.push({
start_offset: chunk.start_offset + last_end,
end_offset: chunk.start_offset + comment.start,
is_covered: false,
})
}
new_chunks.push({
start_offset: chunk.start_offset + comment.start,
end_offset: chunk.start_offset + comment.end,
is_covered: true,
})
last_end = comment.end
}

if (last_end < text.length) {
new_chunks.push({
start_offset: chunk.start_offset + last_end,
end_offset: chunk.end_offset,
is_covered: false,
})
}
}

return merge({ ...stylesheet, chunks: new_chunks })
}

export function chunkify(stylesheet: Coverage): ChunkedCoverage {
let chunks = []
let offset = 0
Expand Down
48 changes: 24 additions & 24 deletions src/lib/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ test.describe('from <style> tag', () => {
let result = calculate_coverage(coverage)
expect.soft(result.total_files_found).toBe(1)
expect.soft(result.total_bytes).toBe(76)
expect.soft(result.covered_bytes).toBe(39)
expect.soft(result.uncovered_bytes).toBe(37)
expect.soft(result.covered_bytes).toBe(57)
expect.soft(result.uncovered_bytes).toBe(19)
expect.soft(result.total_lines).toBe(12)
expect.soft(result.covered_lines).toBe(8)
expect.soft(result.uncovered_lines).toBe(4)
expect.soft(result.line_coverage_ratio).toBe(8 / 12)
expect.soft(result.covered_lines).toBe(9)
expect.soft(result.uncovered_lines).toBe(3)
expect.soft(result.line_coverage_ratio).toBe(9 / 12)
expect.soft(result.total_stylesheets).toBe(1)
})

Expand All @@ -44,9 +44,9 @@ test.describe('from <style> tag', () => {
let sheet = result.coverage_per_stylesheet.at(0)!
expect.soft(sheet.url).toBe('http://localhost/test.html')
expect.soft(sheet.total_lines).toBe(12)
expect.soft(sheet.covered_lines).toBe(8)
expect.soft(sheet.uncovered_lines).toBe(4)
expect.soft(sheet.line_coverage_ratio).toBe(8 / 12)
expect.soft(sheet.covered_lines).toBe(9)
expect.soft(sheet.uncovered_lines).toBe(3)
expect.soft(sheet.line_coverage_ratio).toBe(9 / 12)
})
})

Expand Down Expand Up @@ -82,20 +82,20 @@ test.describe('from <link rel="stylesheet">', () => {
let result = calculate_coverage(coverage)
expect.soft(result.total_files_found).toBe(1)
expect.soft(result.total_bytes).toBe(170)
expect.soft(result.covered_bytes).toBe(96)
expect.soft(result.uncovered_bytes).toBe(74)
expect.soft(result.covered_bytes).toBe(132)
expect.soft(result.uncovered_bytes).toBe(38)
expect.soft(result.total_lines).toBe(23)
expect.soft(result.covered_lines).toBe(15)
expect.soft(result.uncovered_lines).toBe(8)
expect.soft(result.line_coverage_ratio).toBe(15 / 23)
expect.soft(result.covered_lines).toBe(17)
expect.soft(result.uncovered_lines).toBe(6)
expect.soft(result.line_coverage_ratio).toBe(17 / 23)
expect.soft(result.total_stylesheets).toBe(1)
})

test('calculates stats per stylesheet', () => {
let result = calculate_coverage(coverage)
let sheet = result.coverage_per_stylesheet.at(0)!
expect.soft(sheet.covered_lines).toBe(15)
expect.soft(sheet.uncovered_lines).toBe(8)
expect.soft(sheet.covered_lines).toBe(17)
expect.soft(sheet.uncovered_lines).toBe(6)
expect.soft(sheet.total_lines).toBe(23)
expect.soft(sheet.url).toEqual('http://localhost/style.css')
expect
Expand All @@ -108,10 +108,10 @@ test.describe('from <link rel="stylesheet">', () => {
)
.toEqual([
{ is_covered: true, start_line: 1, end_line: 4 },
{ is_covered: false, start_line: 5, end_line: 8 },
{ is_covered: true, start_line: 9, end_line: 13 },
{ is_covered: false, start_line: 14, end_line: 17 },
{ is_covered: true, start_line: 18, end_line: 23 },
{ is_covered: false, start_line: 5, end_line: 7 },
{ is_covered: true, start_line: 8, end_line: 13 },
{ is_covered: false, start_line: 14, end_line: 16 },
{ is_covered: true, start_line: 17, end_line: 23 },
])
})
})
Expand Down Expand Up @@ -146,8 +146,8 @@ test.describe('from coverage data downloaded directly from the browser as JSON',

test('counts totals', () => {
let result = calculate_coverage(coverage)
expect.soft(result.covered_lines).toBe(11)
expect.soft(result.uncovered_lines).toBe(4)
expect.soft(result.covered_lines).toBe(12)
expect.soft(result.uncovered_lines).toBe(3)
expect.soft(result.total_lines).toBe(15)
expect.soft(result.total_stylesheets).toBe(1)
})
Expand All @@ -159,8 +159,8 @@ test.describe('from coverage data downloaded directly from the browser as JSON',
color: blue;
font-size: 24px;
}

/* not covered */

p {
color: red;
}
Expand All @@ -184,8 +184,8 @@ p {
total_lines,
})),
).toEqual([
{ is_covered: true, start_line: 1, end_line: 5, total_lines: 5 },
{ is_covered: false, start_line: 6, end_line: 9, total_lines: 4 },
{ is_covered: true, start_line: 1, end_line: 6, total_lines: 6 },
{ is_covered: false, start_line: 7, end_line: 9, total_lines: 3 },
{ is_covered: true, start_line: 10, end_line: 15, total_lines: 6 },
])
})
Expand Down
6 changes: 4 additions & 2 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { prettify, type PrettifiedChunk, type PrettifiedCoverage } from './prett
import { deduplicate_entries } from './decuplicate.js'
import { filter_coverage } from './filter-entries.js'
import { extend_ranges } from './extend-ranges.js'
import { chunkify, type ChunkedCoverage } from './chunkify.js'
import { chunkify, mark_comments_as_covered, type ChunkedCoverage } from './chunkify.js'

export type CoverageData = {
uncovered_bytes: number
Expand Down Expand Up @@ -81,7 +81,9 @@ export function calculate_coverage(coverage: Coverage[]): CoverageResult {
)
let deduplicated: Coverage[] = deduplicate_entries(filtered_coverage)
let extended: Coverage[] = deduplicated.map((coverage) => extend_ranges(coverage))
let chunkified: ChunkedCoverage[] = extended.map((sheet) => chunkify(sheet))
let chunkified: ChunkedCoverage[] = extended.map((sheet) =>
mark_comments_as_covered(chunkify(sheet)),
)
let prettified: PrettifiedCoverage[] = chunkified.map((sheet) => prettify(sheet))
let coverage_per_stylesheet = prettified.map((stylesheet) =>
calculate_stylesheet_coverage(stylesheet),
Expand Down
75 changes: 57 additions & 18 deletions src/lib/test/kitchen-sink.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ test.describe('comment coverage', () => {
</html>
`

test('leading line comment is marked as uncovered', async () => {
test('leading line comment is marked as covered', async () => {
let css = `
/* start comment */
h1 { color: blue; }
Expand All @@ -81,13 +81,10 @@ test.describe('comment coverage', () => {
end_line,
total_lines,
})),
).toEqual([
{ is_covered: false, start_line: 1, end_line: 1, total_lines: 1 },
{ is_covered: true, start_line: 2, end_line: 5, total_lines: 4 },
])
).toEqual([{ is_covered: true, start_line: 1, end_line: 4, total_lines: 4 }])
})

test('leading block comment is marked as uncovered', async () => {
test('leading block comment is marked as covered', async () => {
let css = `
/*
start comment
Expand All @@ -104,13 +101,10 @@ test.describe('comment coverage', () => {
end_line,
total_lines,
})),
).toEqual([
{ is_covered: false, start_line: 1, end_line: 3, total_lines: 3 },
{ is_covered: true, start_line: 4, end_line: 7, total_lines: 4 },
])
).toEqual([{ is_covered: true, start_line: 1, end_line: 6, total_lines: 6 }])
})

test('trailing line comment is marked as uncovered', async () => {
test('trailing line comment is marked as covered', async () => {
let css = `
h1 { color: blue; }
/* start comment */
Expand All @@ -125,13 +119,10 @@ test.describe('comment coverage', () => {
end_line,
total_lines,
})),
).toEqual([
{ is_covered: true, start_line: 1, end_line: 4, total_lines: 4 },
{ is_covered: false, start_line: 5, end_line: 5, total_lines: 1 },
])
).toEqual([{ is_covered: true, start_line: 1, end_line: 4, total_lines: 4 }])
})

test('trailing block comment is marked as uncovered', async () => {
test('trailing block comment is marked as covered', async () => {
let css = `
h1 { color: blue; }
/*
Expand All @@ -141,6 +132,25 @@ test.describe('comment coverage', () => {
let coverage = (await generate_coverage(html, { link_css: css })) as Coverage[]
let result = calculate_coverage(coverage)
let sheet = result.coverage_per_stylesheet.at(0)!
expect(
sheet.chunks.map(({ is_covered, start_line, end_line, total_lines }) => ({
is_covered,
start_line,
end_line,
total_lines,
})),
).toEqual([{ is_covered: true, start_line: 1, end_line: 6, total_lines: 6 }])
})

test('comment between covered and uncovered rule is marked as covered', async () => {
let css = `
h1 { color: blue; }
/* middle comment */
h2 { color: red; }
`
let coverage = (await generate_coverage(html, { link_css: css })) as Coverage[]
let result = calculate_coverage(coverage)
let sheet = result.coverage_per_stylesheet.at(0)!
expect(
sheet.chunks.map(({ is_covered, start_line, end_line, total_lines }) => ({
is_covered,
Expand All @@ -149,10 +159,39 @@ test.describe('comment coverage', () => {
total_lines,
})),
).toEqual([
{ is_covered: true, start_line: 1, end_line: 4, total_lines: 4 },
{ is_covered: false, start_line: 5, end_line: 7, total_lines: 3 },
{ is_covered: true, start_line: 1, end_line: 5, total_lines: 5 },
{ is_covered: false, start_line: 6, end_line: 8, total_lines: 3 },
])
})

test('multiple adjacent comments are each marked as covered', async () => {
let css = `
/* first comment */
/* second comment */
h1 { color: blue; }
`
let coverage = (await generate_coverage(html, { link_css: css })) as Coverage[]
let result = calculate_coverage(coverage)
let sheet = result.coverage_per_stylesheet.at(0)!
expect(
sheet.chunks.map(({ is_covered, start_line, end_line, total_lines }) => ({
is_covered,
start_line,
end_line,
total_lines,
})),
).toEqual([{ is_covered: true, start_line: 1, end_line: 5, total_lines: 5 }])
})

test('stylesheet with only comments is fully covered', async () => {
let css = `
/* just a comment */
`
let coverage = (await generate_coverage(html, { link_css: css })) as Coverage[]
let result = calculate_coverage(coverage)
let sheet = result.coverage_per_stylesheet.at(0)!
expect(sheet.chunks.every(({ is_covered }) => is_covered)).toBe(true)
})
})

test.describe('@rules', () => {
Expand Down
Loading