diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
index a1641a7..8a40dca 100644
--- a/.github/workflows/playwright.yml
+++ b/.github/workflows/playwright.yml
@@ -37,4 +37,30 @@ jobs:
with:
name: css-coverage
path: css-coverage/*.json
- retention-days: 30
\ No newline at end of file
+ retention-days: 30
+
+ css-coverage:
+ name: Verify CSS Code Coverage
+ runs-on: ubuntu-latest
+ needs: test
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v5
+ with:
+ cache: 'npm'
+ node-version: '>=22.18.0'
+
+ - name: Install dependencies
+ run: npm install --no-fund --no-audit --ignore-scripts
+
+ - name: Download artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: css-coverage
+ path: ./css-coverage
+
+ - name: Analyze CSS Code Coverage
+ run: node scripts/analyze-css-coverage.ts --coverageDir=./css-coverage --minLineCoverage=.8 --minFileLineCoverage=.62 --showUncovered=violations
diff --git a/scripts/analyze-css-coverage.ts b/scripts/analyze-css-coverage.ts
new file mode 100644
index 0000000..a713940
--- /dev/null
+++ b/scripts/analyze-css-coverage.ts
@@ -0,0 +1,180 @@
+import * as fs from 'node:fs'
+import * as path from 'node:path'
+import { calculate_coverage } from '../src/lib/components/coverage/calculate-coverage.ts'
+import { DOMParser } from 'linkedom'
+import type { Coverage } from '../src/lib/components/coverage/types.ts'
+import { parseArgs, styleText } from 'node:util'
+import * as v from 'valibot'
+import { parse_json } from '../src/lib/components/coverage/parse-coverage.ts'
+
+// TODO: architecture
+// Create CLI that
+// - creates cli config
+// - validates cli input against schema
+// Create Program() that
+// - takes input (parsed and validated args)
+// - runs and returns calculations
+// - compares it with arguments passed
+// - returns a Result (passed: boolean, violations, etc.)
+// Create a printer that accepts a formatter
+// - formats can be tap, pretty (default), minimal etc.
+
+let args = process.argv.slice(2)
+
+let { values } = parseArgs({
+ args,
+ allowPositionals: true,
+ options: {
+ // TODO: allow glob?
+ // TODO: convert to coveragedir, min-line-coverage, etc.
+ coverageDir: {
+ type: 'string'
+ },
+ minLineCoverage: {
+ type: 'string'
+ },
+ minFileLineCoverage: {
+ type: 'string',
+ default: '0'
+ },
+ showUncovered: {
+ type: 'string',
+ default: 'none'
+ }
+ }
+})
+
+const showUncoveredOptions = {
+ none: 'none',
+ all: 'all',
+ violations: 'violations'
+} as const
+
+let valuesSchema = v.object({
+ coverageDir: v.pipe(v.string(), v.nonEmpty()),
+ // Coerce args string to number and validate that it's between 0 and 1
+ minLineCoverage: v.pipe(v.string(), v.transform(Number), v.number(), v.minValue(0), v.maxValue(1)),
+ // Coerce args string to number and validate that it's between 0 and 1
+ minFileLineCoverage: v.optional(v.pipe(v.string(), v.transform(Number), v.number(), v.minValue(0), v.maxValue(1))),
+ showUncovered: v.optional(v.pipe(v.string(), v.enum(showUncoveredOptions)), 'none')
+})
+
+let parse_result = v.safeParse(valuesSchema, values)
+if (!parse_result.success) {
+ console.error(styleText(['red', 'bold'], 'Failure'), ': invalid arguments')
+ for (let issue of parse_result.issues) {
+ console.error(`- ${issue.path?.map((p) => p.key).join('.')}: ${issue.message}`)
+ }
+ process.exit(1)
+}
+let { coverageDir, minLineCoverage, minFileLineCoverage, showUncovered } = parse_result.output
+
+function parse_html(html: string) {
+ return new DOMParser().parseFromString(html, 'text/html')
+}
+
+let files = fs.readdirSync(coverageDir)
+
+if (files.length === 0) {
+ console.error(styleText(['red', 'bold'], 'Failure'), `: no JSON files found in ${coverageDir}`)
+ process.exit(1)
+}
+
+console.log(`Checking ${files.length} files...`)
+
+let data = files.reduce((all_files, file_path) => {
+ if (!file_path.endsWith('.json')) return all_files
+ try {
+ let content = fs.readFileSync(path.resolve(coverageDir, file_path), 'utf-8')
+ let parsed = parse_json(content)
+ all_files.push(...parsed)
+ return all_files
+ } catch {
+ return all_files
+ }
+}, [] as Coverage[])
+
+let result = calculate_coverage(data, parse_html)
+
+console.log(`Analyzed ${result.files_found} coverage entries`)
+
+// Verify minLineCoverage
+if (result.line_coverage_ratio >= minLineCoverage) {
+ console.log(
+ `${styleText(['bold', 'green'], 'Success')}: total line coverage is ${(result.line_coverage_ratio * 100).toFixed(2)}%`
+ )
+} else {
+ console.error(
+ `${styleText(['bold', 'red'], 'Failed')}: line coverage is ${(result.line_coverage_ratio * 100).toFixed(2)}% which is lower than the threshold of ${minLineCoverage}`
+ )
+ process.exitCode = 1
+}
+
+// Verify minFileLineCoverage
+if (minFileLineCoverage !== undefined && minFileLineCoverage !== 0) {
+ if (result.coverage_per_stylesheet.some((sheet) => sheet.line_coverage_ratio < minFileLineCoverage)) {
+ console.error(
+ `${styleText(['bold', 'red'], 'Failed')}: Not all files meet the minimum line coverage of ${minFileLineCoverage * 100}%:`
+ )
+ process.exitCode = 1
+ } else {
+ console.log(
+ `${styleText(['bold', 'green'], 'Success')}: all files pass minFileLineCoverage of ${minFileLineCoverage * 100}%`
+ )
+ }
+}
+
+if (showUncovered !== 'none') {
+ const NUM_LEADING_LINES = 3
+ const NUM_TRAILING_LINES = NUM_LEADING_LINES
+ let terminal_width = process.stdout.columns || 80
+ let line_number = (num: number, covered: boolean = true) =>
+ `${num.toString().padStart(5, ' ')} ${covered ? '│' : '☓'} `
+
+ for (let sheet of result.coverage_per_stylesheet) {
+ if (
+ (sheet.line_coverage_ratio !== 1 && showUncovered === 'all') ||
+ (minFileLineCoverage !== undefined &&
+ minFileLineCoverage !== 0 &&
+ sheet.line_coverage_ratio < minFileLineCoverage &&
+ showUncovered === 'violations')
+ ) {
+ console.log()
+ console.log(styleText('dim', '─'.repeat(terminal_width)))
+ console.log(sheet.url)
+ console.log(
+ `Coverage: ${(sheet.line_coverage_ratio * 100).toFixed(2)}%, ${sheet.covered_lines}/${sheet.total_lines} lines covered`
+ )
+ if (minFileLineCoverage) {
+ let lines_to_cover = minFileLineCoverage * sheet.total_lines - sheet.covered_lines
+ console.log(
+ `💡 Cover ${Math.ceil(lines_to_cover)} more lines to meet the file threshold of ${minFileLineCoverage * 100}%`
+ )
+ }
+ console.log(styleText('dim', '─'.repeat(terminal_width)))
+
+ let lines = sheet.text.split('\n')
+ let line_coverage = sheet.line_coverage
+
+ for (let i = 0; i < lines.length; i++) {
+ if (line_coverage[i] === 0) {
+ // Rewind cursor N lines to render N previous lines
+ for (let j = i - NUM_LEADING_LINES; j < i; j++) {
+ console.log(styleText('dim', line_number(j)), styleText('dim', lines[j]))
+ }
+ // Render uncovered lines while increasing cursor until reaching next covered block
+ while (line_coverage[i] === 0) {
+ console.log(styleText('red', line_number(i, false)), lines[i])
+ i++
+ }
+ // Forward cursor N lines to render N trailing lines
+ for (let end = i + NUM_TRAILING_LINES; i < end && i < lines.length; i++) {
+ console.log(styleText('dim', line_number(i)), styleText('dim', lines[i]))
+ }
+ // Show empty line between blocks
+ console.log()
+ }
+ }
+ }
+ }
+}
diff --git a/src/lib/components/coverage/Coverage.spec.ts b/src/lib/components/coverage/Coverage.spec.ts
index a34339c..04ad5bc 100644
--- a/src/lib/components/coverage/Coverage.spec.ts
+++ b/src/lib/components/coverage/Coverage.spec.ts
@@ -57,7 +57,7 @@ test.describe('loading example file', () => {
.toHaveAccessibleName('https://www.projectwallace.com/_app/immutable/assets/0.BBE7cspC.css')
await expect.soft(first_row.getByRole('cell').nth(1)).toHaveAccessibleName('38 kB')
await expect.soft(first_row.getByRole('cell').nth(2)).toHaveAccessibleName('2,619')
- await expect.soft(first_row.getByRole('cell').nth(3)).toHaveAccessibleName('72.16%')
+ await expect.soft(first_row.getByRole('cell').nth(3)).toHaveAccessibleName('74.95%')
// Elements in correct state:
await expect.soft(table.getByRole('row').nth(1)).toHaveAttribute('aria-selected', 'true')
@@ -73,8 +73,8 @@ test.describe('loading example file', () => {
await expect.soft(ranges.nth(3)).toHaveClass(/uncovered/)
await expect.soft(ranges.nth(0)).toHaveText('1 2 3 4 5 6 7 8 9 10 11 12 13 14')
- await expect.soft(ranges.nth(1)).toHaveText('15 16 17 18')
- await expect.soft(ranges.nth(3)).toHaveText('51 52 53 54 55 56')
+ await expect.soft(ranges.nth(1)).toHaveText('15 16 17')
+ await expect.soft(ranges.nth(3)).toHaveText('51 52 53 54 55')
})
test('selecting a row', async ({ page }) => {
@@ -109,7 +109,7 @@ test.describe('loading example file', () => {
.soft(first_row.getByRole('cell').nth(0))
.toHaveAccessibleName('https://www.projectwallace.com/_app/immutable/assets/0.BBE7cspC.css')
await expect.soft(first_row.getByRole('cell').nth(1)).toHaveAccessibleName('38 kB')
- await expect.soft(first_row.getByRole('cell').nth(3)).toHaveAccessibleName('72.16%')
+ await expect.soft(first_row.getByRole('cell').nth(3)).toHaveAccessibleName('74.95%')
})
test('sorting by total size', async ({ page }) => {
@@ -123,7 +123,7 @@ test.describe('loading example file', () => {
.soft(first_row.getByRole('cell').nth(0))
.toHaveAccessibleName('https://www.projectwallace.com/_app/immutable/assets/0.BBE7cspC.css')
await expect.soft(first_row.getByRole('cell').nth(1)).toHaveAccessibleName('38 kB')
- await expect.soft(first_row.getByRole('cell').nth(3)).toHaveAccessibleName('72.16%')
+ await expect.soft(first_row.getByRole('cell').nth(3)).toHaveAccessibleName('74.95%')
let css_content = await page.getByTestId('pre-css').textContent()
expect.soft(css_content).toContain('.logo.svelte-1jiwtxp {')
diff --git a/src/lib/components/coverage/Coverage.svelte b/src/lib/components/coverage/Coverage.svelte
index e2faedd..53756f6 100644
--- a/src/lib/components/coverage/Coverage.svelte
+++ b/src/lib/components/coverage/Coverage.svelte
@@ -60,7 +60,9 @@
return sort_direction === 'asc' ? a.total_bytes - b.total_bytes : b.total_bytes - a.total_bytes
}
if (sort_by === 'coverage') {
- return sort_direction === 'asc' ? a.coverage_ratio - b.coverage_ratio : b.coverage_ratio - a.coverage_ratio
+ return sort_direction === 'asc'
+ ? a.line_coverage_ratio - b.line_coverage_ratio
+ : b.line_coverage_ratio - a.line_coverage_ratio
}
if (sort_by === 'name') {
return sort_direction === 'asc' ? string_sort(a.url, b.url) : string_sort(b.url, a.url)
@@ -110,12 +112,12 @@
Coverage
- {format_percentage(calculated.coverage_ratio)}
- {format_percentage(calculated.line_coverage)} of lines
+ {format_percentage(calculated.byte_coverage_ratio)}
+ {format_percentage(calculated.line_coverage_ratio)} of lines
Total
- {format_filesize(calculated.used_bytes + calculated.unused_bytes)}
+ {format_filesize(calculated.total_bytes)}
{format_number(calculated.total_lines)} lines
@@ -152,17 +154,17 @@
{#each sorted_items as item_index, index}
{@const stylesheet = calculated.coverage_per_stylesheet[item_index]}
- {@const { url, total_bytes, total_lines, coverage_ratio, covered_lines } = stylesheet}
+ {@const { url, total_bytes, total_lines, line_coverage_ratio, covered_lines } = stylesheet}
|
{url}
|
{format_filesize(total_bytes)} |
{format_number(total_lines)} |
- {format_percentage(coverage_ratio)} |
+ {format_percentage(line_coverage_ratio)} |
-
+
|
diff --git a/src/lib/components/coverage/calculate-coverage.spec.ts b/src/lib/components/coverage/calculate-coverage.spec.ts
index 7059796..82466cb 100644
--- a/src/lib/components/coverage/calculate-coverage.spec.ts
+++ b/src/lib/components/coverage/calculate-coverage.spec.ts
@@ -170,12 +170,12 @@ test.describe('calculates coverage', () => {
let result = calculate_coverage(coverage, html_parser)
expect.soft(result.files_found).toBe(1)
expect.soft(result.total_bytes).toBe(80)
- expect.soft(result.used_bytes).toBe(37)
- expect.soft(result.unused_bytes).toBe(41)
+ expect.soft(result.used_bytes).toBe(42)
+ expect.soft(result.unused_bytes).toBe(38)
expect.soft(result.total_lines).toBe(11)
expect.soft(result.covered_lines).toBe(7)
expect.soft(result.uncovered_lines).toBe(11 - 7)
- expect.soft(result.line_coverage).toBe(7 / 11)
+ expect.soft(result.line_coverage_ratio).toBe(7 / 11)
})
test('calculates stats per stylesheet', () => {
@@ -189,7 +189,7 @@ test.describe('calculates coverage', () => {
expect.soft(sheet.total_lines).toBe(11)
expect.soft(sheet.covered_lines).toBe(7)
expect.soft(sheet.uncovered_lines).toBe(4)
- expect.soft(sheet.coverage_ratio).toBe(7 / 11)
+ expect.soft(sheet.line_coverage_ratio).toBe(7 / 11)
expect.soft(sheet.line_coverage).toEqual(new Uint8Array([1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1]))
})
})
@@ -226,12 +226,12 @@ test.describe('calculates coverage', () => {
let result = calculate_coverage(coverage, html_parser)
expect.soft(result.files_found).toBe(1)
expect.soft(result.total_bytes).toBe(174)
- expect.soft(result.used_bytes).toBe(75)
- expect.soft(result.unused_bytes).toBe(93)
+ expect.soft(result.used_bytes).toBe(91)
+ expect.soft(result.unused_bytes).toBe(83)
expect.soft(result.total_lines).toBe(21)
expect.soft(result.covered_lines).toBe(12)
expect.soft(result.uncovered_lines).toBe(21 - 12)
- expect.soft(result.line_coverage).toBe(12 / 21)
+ expect.soft(result.line_coverage_ratio).toBe(12 / 21)
})
test('calculates stats per stylesheet', () => {
@@ -247,7 +247,7 @@ test.describe('calculates coverage', () => {
expect.soft(sheet.total_lines).toBe(21)
expect.soft(sheet.covered_lines).toBe(12)
expect.soft(sheet.uncovered_lines).toBe(21 - 12)
- expect.soft(sheet.coverage_ratio).toBe(12 / 21)
+ expect.soft(sheet.line_coverage_ratio).toBe(12 / 21)
expect
.soft(sheet.line_coverage)
.toEqual(new Uint8Array([1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1]))
@@ -284,10 +284,10 @@ test.describe('calculates coverage', () => {
test('counts totals', () => {
let result = calculate_coverage(coverage, html_parser)
- expect.soft(result.covered_lines).toBe(8)
- expect.soft(result.uncovered_lines).toBe(6)
+ expect.soft(result.covered_lines).toBe(9)
+ expect.soft(result.uncovered_lines).toBe(5)
expect.soft(result.total_lines).toBe(14)
- expect.soft(result.line_coverage).toBe(8 / 14)
+ expect.soft(result.line_coverage_ratio).toBe(9 / 14)
})
test('extracts and formats css', () => {
@@ -318,11 +318,11 @@ test.describe('calculates coverage', () => {
// h1 {}
1, 1, 1, 1,
// comment + p {}
- 0, 0, 0, 0, 0,
+ 0, 0, 0, 0,
// @media
1,
// h1 {
- 0,
+ 1, 0,
// color: green; }
1, 1, 1
])
diff --git a/src/lib/components/coverage/calculate-coverage.ts b/src/lib/components/coverage/calculate-coverage.ts
index 385e79c..f029d26 100644
--- a/src/lib/components/coverage/calculate-coverage.ts
+++ b/src/lib/components/coverage/calculate-coverage.ts
@@ -1,6 +1,6 @@
-import type { Coverage, Range } from './types'
-import { prettify } from './prettify'
-import { ext } from './ext'
+import type { Coverage, Range } from './types.ts'
+import { prettify } from './prettify.ts'
+import { ext } from './ext.ts'
import type { HTMLDocument } from 'linkedom/types/html/document'
interface HtmlParser {
@@ -127,6 +127,29 @@ export function deduplicate_entries(
return checked_stylesheets
}
+type CoverageData = {
+ unused_bytes: number
+ used_bytes: number
+ total_bytes: number
+ line_coverage_ratio: number
+ byte_coverage_ratio: number
+ total_lines: number
+ covered_lines: number
+ uncovered_lines: number
+}
+
+export type StylesheetCoverage = CoverageData & {
+ url: string
+ text: string
+ ranges: Range[]
+ line_coverage: Uint8Array
+}
+
+export type CoverageResult = CoverageData & {
+ files_found: number
+ coverage_per_stylesheet: StylesheetCoverage[]
+}
+
/**
* @description
* CSS Code Coverage calculation
@@ -138,99 +161,83 @@ export function deduplicate_entries(
* 4. Calculate used/unused CSS bytes (fastest path, no inspection of the actual CSS needed)
* 5. Calculate line-coverage, byte-coverage per stylesheet
*/
-
-// TODO: add flag for prettification on/off
-// When disabled we can skip the prettify step as well as recalculating the ranges in HTML (get_css_and_ranges_from_html)
-// This also means that when pretty=true, parse_html MUST also be included. parse_html is optional when pretty=false
-export function calculate_coverage(browser_coverage: Coverage[], parse_html: HtmlParser) {
- let total_bytes = 0
- let used_bytes = 0
- let unused_bytes = 0
- let total_lines = 0
- let covered_lines = 0
- let uncovered_lines = 0
- let files_found = browser_coverage.length
- let filtered_coverage = filter_coverage(browser_coverage, parse_html)
+export function calculate_coverage(coverage: Coverage[], parse_html: HtmlParser): CoverageResult {
+ let files_found = coverage.length
+ let filtered_coverage = filter_coverage(coverage, parse_html)
let prettified_coverage = prettify(filtered_coverage)
let deduplicated = deduplicate_entries(prettified_coverage)
- // SECTION: calculate used vs. unused bytes
- // We sort the ranges by their start position
- // Then we iterate over the ranges and calculate the used bytes
- for (let [text, { ranges }] of deduplicated) {
- total_bytes += text.length
- let last_position = 0
- ranges.sort((a, b) => a.start - b.start)
- for (let range of ranges) {
- if (range.start > last_position) {
- let unused_text = text.slice(last_position, range.start)
- unused_bytes += unused_text.length
- }
- used_bytes += range.end - range.start - 1
- last_position = range.end
- }
- }
-
// SECTION: calculate coverage for each individual stylesheet we found
let coverage_per_stylesheet = Array.from(deduplicated).map(([text, { url, ranges }]) => {
- let file_used_bytes = ranges.reduce((acc, range) => acc + (range.end - range.start), 0)
- let trimmed_text = text.trim()
+ function is_line_covered(line: string, start_offset: number) {
+ let end = start_offset + line.length
+ let next_offset = end + 1 // account for newline character
+ let is_empty = /^\s*$/.test(line)
+ let is_closing_brace = line.endsWith('}')
- let lines = trimmed_text.split('\n')
+ if (!is_empty && !is_closing_brace) {
+ for (let range of ranges) {
+ if (range.start > end || range.end < start_offset) {
+ continue
+ }
+ if (range.start <= start_offset && range.end >= end) {
+ return true
+ } else if (line.startsWith('@') && range.start > start_offset && range.start < next_offset) {
+ return true
+ }
+ }
+ }
+ return false
+ }
+
+ let lines = text.split('\n')
let total_file_lines = lines.length
let line_coverage = new Uint8Array(total_file_lines)
let file_lines_covered = 0
+ let file_total_bytes = text.length
+ let file_bytes_covered = 0
let offset = 0
- let index = 0
- for (let line of lines) {
+
+ for (let index = 0; index < lines.length; index++) {
+ let line = lines[index]
let start = offset
let end = offset + line.length
let next_offset = end + 1 // +1 for the newline character
- let is_in_range = false
- let trimmed_line = line.trim()
- let is_empty = trimmed_line.length === 0
- let is_closing_brace = !is_empty && trimmed_line === '}'
+ let is_empty = /^\s*$/.test(line)
+ let is_closing_brace = line.endsWith('}')
+ let is_in_range = is_line_covered(line, start)
+ let is_covered = false
- if (!is_empty && !is_closing_brace) {
- for (let range of ranges) {
- if (range.start <= start && range.end >= end) {
- is_in_range = true
- break
- } else if (trimmed_line.startsWith('@') && range.start > start && range.start < next_offset) {
- is_in_range = true
- break
- }
- }
- }
-
- let prev_is_covered = index > 0 ? line_coverage[index - 1] === 1 : false
+ let prev_is_covered = index > 0 && line_coverage[index - 1] === 1
if (is_in_range && !is_closing_brace && !is_empty) {
- file_lines_covered++
- line_coverage[index] = 1
+ is_covered = true
} else if ((is_empty || is_closing_brace) && prev_is_covered) {
+ is_covered = true
+ } else if (is_empty && !prev_is_covered && is_line_covered(lines[index + 1], next_offset)) {
+ // If the next line is covered, mark this empty line as covered
+ is_covered = true
+ }
+
+ line_coverage[index] = is_covered ? 1 : 0
+
+ if (is_covered) {
file_lines_covered++
- line_coverage[index] = 1
- } else if (is_empty && line_coverage[index - 1] === 0) {
- line_coverage[index] = 0
- } else {
- line_coverage[index] = 0
+ file_bytes_covered += line.length + 1
}
+
offset = next_offset
- index++
}
- total_lines += total_file_lines
- covered_lines += file_lines_covered
- uncovered_lines += total_file_lines - file_lines_covered
-
return {
url,
- text: trimmed_text,
+ text,
ranges,
- used_bytes: file_used_bytes,
- total_bytes: trimmed_text.length,
- coverage_ratio: file_lines_covered / total_file_lines,
+ unused_bytes: file_total_bytes - file_bytes_covered,
+ used_bytes: file_bytes_covered,
+ total_bytes: file_total_bytes,
+ line_coverage_ratio: file_lines_covered / total_file_lines,
+ byte_coverage_ratio: file_bytes_covered / file_total_bytes,
line_coverage,
total_lines: total_file_lines,
covered_lines: file_lines_covered,
@@ -238,19 +245,37 @@ export function calculate_coverage(browser_coverage: Coverage[], parse_html: Htm
}
})
- let coverage_ratio =
- coverage_per_stylesheet.reduce((acc, sheet) => acc + sheet.coverage_ratio, 0) / coverage_per_stylesheet.length
+ let { total_lines, total_covered_lines, total_uncovered_lines, total_bytes, total_used_bytes, total_unused_bytes } =
+ coverage_per_stylesheet.reduce(
+ (totals, sheet) => {
+ totals.total_lines += sheet.total_lines
+ totals.total_covered_lines += sheet.covered_lines
+ totals.total_uncovered_lines += sheet.uncovered_lines
+ totals.total_bytes += sheet.total_bytes
+ totals.total_used_bytes += sheet.used_bytes
+ totals.total_unused_bytes += sheet.unused_bytes
+ return totals
+ },
+ {
+ total_lines: 0,
+ total_covered_lines: 0,
+ total_uncovered_lines: 0,
+ total_bytes: 0,
+ total_used_bytes: 0,
+ total_unused_bytes: 0
+ }
+ )
return {
files_found,
total_bytes,
total_lines,
- used_bytes,
- covered_lines,
- unused_bytes,
- uncovered_lines,
- coverage_ratio,
- line_coverage: covered_lines / total_lines,
+ used_bytes: total_used_bytes,
+ covered_lines: total_covered_lines,
+ unused_bytes: total_unused_bytes,
+ uncovered_lines: total_uncovered_lines,
+ byte_coverage_ratio: total_used_bytes / total_bytes,
+ line_coverage_ratio: total_covered_lines / total_lines,
coverage_per_stylesheet
}
}
diff --git a/src/lib/components/coverage/parse-coverage.ts b/src/lib/components/coverage/parse-coverage.ts
new file mode 100644
index 0000000..acd5e79
--- /dev/null
+++ b/src/lib/components/coverage/parse-coverage.ts
@@ -0,0 +1,25 @@
+import * as v from 'valibot'
+import type { Coverage } from './types'
+
+let CoverageSchema = v.array(
+ v.object({
+ text: v.undefinedable(v.string()),
+ url: v.string(),
+ ranges: v.array(
+ v.object({
+ start: v.number(),
+ end: v.number()
+ })
+ )
+ })
+)
+
+export function parse_json(input: string) {
+ try {
+ let parse_result = JSON.parse(input)
+ v.parse(CoverageSchema, parse_result)
+ return parse_result as Coverage[]
+ } catch {
+ return [] as Coverage[]
+ }
+}
diff --git a/src/lib/components/coverage/prettify.ts b/src/lib/components/coverage/prettify.ts
index 71ee73d..8d8a23d 100644
--- a/src/lib/components/coverage/prettify.ts
+++ b/src/lib/components/coverage/prettify.ts
@@ -37,46 +37,44 @@ export function prettify(coverage: Coverage[]): Coverage[] {
let index = 0
tokenize(text, (type, start, end) => {
- if (!irrelevant_tokens.has(type)) {
- index++
+ if (irrelevant_tokens.has(type)) return
+ index++
- // format-css changes the Url token to a Function,String,RightParenthesis token sequence
- if (type === tokenTypes.Url) {
- index += 2
- }
+ // format-css changes the Url token to a Function,String,RightParenthesis token sequence
+ if (type === tokenTypes.Url) {
+ index += 2
+ }
- let range_index = is_in_range(start, end)
- if (range_index !== -1) {
- ext_ranges[range_index]!.tokens.push(index)
- }
+ let range_index = is_in_range(start, end)
+ if (range_index !== -1) {
+ ext_ranges[range_index]!.tokens.push(index)
}
})
- let new_tokens: { index: number; start: number; end: number }[] = []
+ let new_tokens: Map = new Map()
index = 0
tokenize(formatted, (type, start, end) => {
- if (!irrelevant_tokens.has(type)) {
- index++
+ if (irrelevant_tokens.has(type)) return
+ index++
- // format-css changes the Url token to a Function,String,RightParenthesis token sequence
- if (type === tokenTypes.Url) {
- index += 2
- }
-
- new_tokens.push({ index, start, end })
+ // format-css changes the Url token to a Function,String,RightParenthesis token sequence
+ if (type === tokenTypes.Url) {
+ index += 2
}
+
+ new_tokens.set(index, { start, end })
})
let new_ranges: Range[] = []
for (let range of ext_ranges) {
- let start_token = new_tokens.find((token) => token.index === range.tokens.at(0))
- let end_token = new_tokens.find((token) => token.index === range.tokens.at(-1))
+ let start_token = new_tokens.get(range.tokens.at(0)!)
+ let end_token = new_tokens.get(range.tokens.at(-1)!)
if (start_token !== undefined && end_token !== undefined) {
new_ranges.push({
- start: start_token?.start,
- end: end_token?.end
+ start: start_token.start,
+ end: end_token.end
})
}
}
diff --git a/src/lib/css/style.css b/src/lib/css/style.css
index 198c175..8368be8 100644
--- a/src/lib/css/style.css
+++ b/src/lib/css/style.css
@@ -342,7 +342,7 @@
/* Special cases */
--uneven-tr-bg: color-mix(in srgb, var(--gray-450) 10%, transparent);
/* Mimics the 'Highlight' CSS system color keyword */
- --highlight: rgb(from light-dark(var(--accent-600), var(--accent)) r g b / 0.12);
+ --highlight: rgb(from light-dark(var(--accent-600), var(--accent)) r g b / 0.14);
/* Setup theme */
color-scheme: dark light;
diff --git a/src/routes/(public)/css-coverage/+page.svelte b/src/routes/(public)/css-coverage/+page.svelte
index bcedb80..b1cc8db 100644
--- a/src/routes/(public)/css-coverage/+page.svelte
+++ b/src/routes/(public)/css-coverage/+page.svelte
@@ -4,37 +4,14 @@
import Label from '$components/Label.svelte'
import Icon from '$components/Icon.svelte'
import Seo from '$components/Seo.svelte'
- import * as v from 'valibot'
import Content from './content.md'
import Markdown from '$components/Markdown.svelte'
import Container from '$components/Container.svelte'
import Heading from '$components/Heading.svelte'
-
- let CoverageSchema = v.array(
- v.object({
- text: v.undefinedable(v.string()),
- url: v.string(),
- ranges: v.array(
- v.object({
- start: v.number(),
- end: v.number()
- })
- )
- })
- )
+ import { parse_json } from '$components/coverage/parse-coverage'
let data: Coverage[] = $state([])
- function parse_json(input: string) {
- try {
- let parse_result = JSON.parse(input)
- v.parse(CoverageSchema, parse_result)
- return parse_result as Coverage[]
- } catch (error) {
- return [] as Coverage[]
- }
- }
-
async function onchange(event: Event) {
let files = (event.target as HTMLInputElement)?.files
let new_data: Coverage[] = []
diff --git a/tsconfig.json b/tsconfig.json
index d1e7390..39f95c1 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -10,6 +10,7 @@
"sourceMap": false,
"strict": true,
"moduleResolution": "node",
+ "allowImportingTsExtensions": true,
// https://svelte.dev/docs/svelte/typescript#tsconfig.json-settings
"target": "ES2022",