// @ts-check import path from 'node:path'; import {writeFile} from '../../tasks/utils.js'; /** @typedef {{text: string; covered: boolean}} CodePart */ /** * @param {string} code * @param {{start: number; end: number}[]} ranges * @returns {CodePart[]} */ function splitCode(code, ranges) { /** @type {CodePart[]} */ const parts = []; if (ranges.length === 0) { parts.push({text: code, covered: false}); return; } if (ranges[0].start > 0) { parts.push({text: code.substring(0, ranges[0].start), covered: false}); } const lastRange = ranges[ranges.length - 1]; for (let i = 0; i < ranges.length; i++) { const range = ranges[i]; parts.push({text: code.substring(range.start, range.end), covered: true}); if (range !== lastRange) { const nextRange = ranges[i + 1]; parts.push({text: code.substring(range.end, nextRange.start), covered: false}); } } if (lastRange.end < code.length) { parts.push({text: code.substring(lastRange.end), covered: false}); } return parts; } function red(/** @type {string} */text) { return `\x1b[31m${text}\x1b[0m`; } function green(/** @type {string} */text) { return `\x1b[32m${text}\x1b[0m`; } /** * @param {string} code * @param {{start: number; end: number}[]} ranges */ export function logCoverage(code, ranges) { code = code.substring(0, code.indexOf('//# sourceMappingURL=')); const parts = splitCode(code, ranges); const message = parts .map(({text, covered}) => (covered ? green : red)(text)) .join(''); console.log(message); } function escapeHTML(/** @type {string} */html) { return html .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** @typedef {{name: string; length: number; coverage: number; parts: {text: string; covered: boolean}[]}} FileCoverageInfo */ /** * @param {string} name * @param {string} code * @param {{start: number; end: number}[]} ranges * @returns {FileCoverageInfo} */ function getCoverageInfo(name, code, ranges) { code = code.substring(0, code.indexOf('//# sourceMappingURL=')); const parts = splitCode(code, ranges); const length = code.length; const coverage = parts.filter((p) => p.covered).reduce((sum, p) => sum + p.text.length, 0) / length; return {name, length, coverage, parts}; } /** * @param {string} dir * @param {FileCoverageInfo} info * @returns {Promise} */ async function generateHTMLCoverageReport(dir, info) { const {name, coverage, parts} = info; /** @type {string[]} */ const lines = []; lines.push(''); lines.push(''); lines.push(''); const title = `Coverage report: ${name}`; lines.push(` ${title}`); lines.push(' '); lines.push(''); lines.push(''); lines.push(`

${title}

`); lines.push(`

Covered ${(coverage * 100).toFixed(0)}%

`); lines.push(` ${parts.map((p) => `${escapeHTML(p.text)}`).join('')}`); lines.push(''); lines.push(''); await writeFile(path.join(dir, `${name}.html`), lines.join('\n')); } /** * @param {string} dir * @param {FileCoverageInfo[]} info * @returns {Promise} */ async function generateIndexHTMLCoveragePage(dir, info) { /** @type {string[]} */ const lines = []; lines.push(''); lines.push(''); lines.push(''); lines.push(` Code coverage reports`); lines.push(' '); lines.push(''); lines.push(''); lines.push(`

Code coverage reports

`); const totalCoverage = info.reduce((sum, i) => sum + i.coverage * i.length, 0); const totalLength = info.reduce((sum, i) => sum + i.length, 0); lines.push(`

Total coverage ${(totalCoverage / totalLength * 100).toFixed(0)}%

`); lines.push(' '); info.forEach((i) => { lines.push(' '); lines.push(` `); lines.push(` `); lines.push(' '); }); lines.push('
${(i.coverage * 100).toFixed(0)}%${i.name}
'); lines.push(''); lines.push(''); await writeFile(path.join(dir, 'index.html'), lines.join('\n')); } /** * @param {string} dir * @param {import('puppeteer-core').CoverageEntry[]} coverage */ export async function generateHTMLCoverageReports(dir, coverage) { const info = coverage .filter(({url}) => url.startsWith('chrome-extension://')) .map(({url, text, ranges}) => { const name = url.replace(/^chrome-extension:\/\/.*?\//, ''); return getCoverageInfo(name, text, ranges); }); await generateIndexHTMLCoveragePage(dir, info); await Promise.all(info.map((i) => generateHTMLCoverageReport(dir, i))); }