Warning
This is a very experimental approach to calculating CSS Code Coverage and currently very much a work in progress.
Takes your generated coverage files and turns them into something actually usable. Accepts coverage reports generated by browsers (Edge/Chrome/chromium), Puppeteer, Playwright.
Features include:
- 🤩 Prettifies CSS for easy inspection and updates coverage ranges after prettification
- 🪄 Marks each line of each CSS file as covered or uncovered
- 📑 A single stylesheet that's reported over multiple URL's is combined into a single one, coverage ranges merged
- 🗂️ Creates a report of total line coverage, byte coverage and coverage details per individual stylesheet discovered
npm install @projectwallace/css-code-coverage
import { calculate_coverage } from '@projectwallace/css-code-coverage' let report = await calculcate_coverage(coverage_data) // => report.line_coverage_ratio: 0.80 // => report.byte_coverage_ratio: 0.85 // => report.total_lines: 1000 // => report.covered_lines: 800 // etc.
See src/index.ts for the data that's returned.
There are two principal ways of collecting CSS Coverage data:
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:
// Start collecting coverage await page.coverage.startCSSCoverage() // Load the page, do all sorts of interactions to increase coverage, etc. await page.goto('http://example.com') // Stop the coverage and store the result in a variable to pass along let coverage = await page.coverage.stopCSSCoverage() // Now we can process it import { calculate_coverage } from '@projectwallace/css-code-coverage' let report = calculcate_coverage(coverage)
Add a cssCoverage fixture to automatically collect and save CSS coverage for each test. Create a fixtures file (e.g. tests/fixtures.ts):
import { test as base, expect } from '@playwright/test' import * as fs from 'node:fs/promises' import * as path from 'node:path' export const test = base.extend({ cssCoverage: [ async ({ page }, use, testInfo) => { // Start collecting CSS coverage before the test runs await page.coverage.startCSSCoverage() // Run the test await use() // Collect the coverage data after the test finishes let coverage = await page.coverage.stopCSSCoverage() // Build a unique, human-readable filename from the test's title path, // replacing characters that are invalid or ambiguous in file names let file_name = testInfo.titlePath .map((s) => s .replaceAll(/\s+|\/|\./g, '-') .replaceAll(/[^a-z0-9-_]/gi, '') .toLowerCase(), ) .join('-') + '.json' // Write the coverage data to disk let dir = path.join(process.cwd(), 'css-coverage') await fs.mkdir(dir, { recursive: true }) let file_path = path.join(dir, file_name) await fs.writeFile(file_path, JSON.stringify(coverage)) // Attach the file to the Playwright HTML report for easy inspection await testInfo.attach('css-coverage', { path: file_path, contentType: 'application/json' }) }, // auto: true makes this fixture run for every test without needing // to explicitly request it as a parameter { auto: true }, ], }) export { expect }
Import test from your fixtures file instead of @playwright/test and coverage is collected automatically for every test:
import { test, expect } from './fixtures.js' test('my test', async ({ page }) => { await page.goto('https://example.com') })
Pass the output directory to the css-coverage CLI to analyze results:
css-coverage --coverage-dir=./css-coverage --min-coverage=0.8
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:
- Collect coverage in Microsoft Edge: https://learn.microsoft.com/en-us/microsoft-edge/devtools-guide-chromium/coverage/
- Collect coverage in Google Chrome: https://developer.chrome.com/docs/devtools/coverage/
Additionally, DevTools Tips writes about it in their explainer.
You end up with one or more JSON files that contain coverage data. We provide a helper parse_coverage() that both parses the JSON and validates it so you can pass it directly into calculate_coverage().
// Read a single JSON or a folder full of JSON files with coverage data // Coverage data looks like this: // { // url: 'https://www.projectwallace.com/style.css', // text: 'a { color: blue; text-decoration: underline; }', etc. // ranges: [ // { start: 0, end: 46 } // ] // } import { parse_coverage } from '@projectwallace/css-code-coverage' let files = await fs.glob('./css-coverage/**/*.json') let coverage_data = [] for (let file of files) { let json_content = await fs.readFile(file, 'urf-8') coverage_data.push(...parse_coverage(json_content)) }
Use the CLI tool (css-coverage) to check if coverage meets minimum requirements, globally and/or per file.
css-coverage <coverage-dir> --min-coverage=<number> [options]