UNPKG

@cypress/code-coverage

Version:

Saves the code coverage collected during Cypress tests

246 lines (207 loc) 7.4 kB
// @ts-check const istanbul = require('istanbul-lib-coverage') const { join, resolve } = require('path') const { existsSync, mkdirSync, readFileSync, writeFileSync } = require('fs') const execa = require('execa') const { showNycInfo, resolveRelativePaths, checkAllPathsNotFound, tryFindingLocalFiles, readNycOptions, includeAllFiles } = require('./task-utils') const { fixSourcePaths } = require('./support-utils') const debug = require('debug')('code-coverage') // these are standard folder and file names used by NYC tools const processWorkingDirectory = process.cwd() // there might be custom "nyc" options in the user package.json // see https://github.com/istanbuljs/nyc#configuring-nyc // potentially there might be "nyc" options in other configuration files // it allows, but for now ignore those options const pkgFilename = join(processWorkingDirectory, 'package.json') const pkg = existsSync(pkgFilename) ? JSON.parse(readFileSync(pkgFilename, 'utf8')) : {} const scripts = pkg.scripts || {} const DEFAULT_CUSTOM_COVERAGE_SCRIPT_NAME = 'coverage:report' const customNycReportScript = scripts[DEFAULT_CUSTOM_COVERAGE_SCRIPT_NAME] const nycReportOptions = (function getNycOption() { // https://github.com/istanbuljs/nyc#common-configuration-options const nycReportOptions = readNycOptions(processWorkingDirectory) if (nycReportOptions.exclude && !Array.isArray(nycReportOptions.exclude)) { console.error('NYC options: %o', nycReportOptions) throw new Error('Expected "exclude" to be an array') } if (nycReportOptions['temp-dir']) { nycReportOptions['temp-dir'] = resolve(nycReportOptions['temp-dir']) } else { nycReportOptions['temp-dir'] = join(processWorkingDirectory, '.nyc_output') } nycReportOptions.tempDir = nycReportOptions['temp-dir'] if (nycReportOptions['report-dir']) { nycReportOptions['report-dir'] = resolve(nycReportOptions['report-dir']) } // seems nyc API really is using camel cased version nycReportOptions.reportDir = nycReportOptions['report-dir'] return nycReportOptions })() const nycFilename = join(nycReportOptions['temp-dir'], 'out.json') let coverageMap = (() => { const previousCoverage = existsSync(nycFilename) ? JSON.parse(readFileSync(nycFilename, 'utf8')) : {} return istanbul.createCoverageMap(previousCoverage) })() function saveCoverage(coverage) { if (!existsSync(nycReportOptions.tempDir)) { mkdirSync(nycReportOptions.tempDir, { recursive: true }) debug('created folder %s for output coverage', nycReportOptions.tempDir) } writeFileSync(nycFilename, JSON.stringify(coverage, null, 2)) } function maybePrintFinalCoverageFiles(folder) { const jsonReportFilename = join(folder, 'coverage-final.json') if (!existsSync(jsonReportFilename)) { debug('Did not find final coverage file %s', jsonReportFilename) return } debug('Final coverage in %s', jsonReportFilename) const finalCoverage = JSON.parse(readFileSync(jsonReportFilename, 'utf8')) const finalCoverageKeys = Object.keys(finalCoverage) debug( 'There are %d key(s) in %s', finalCoverageKeys.length, jsonReportFilename ) finalCoverageKeys.forEach((key) => { const s = finalCoverage[key].s || {} const statements = Object.keys(s) const totalStatements = statements.length let coveredStatements = 0 statements.forEach((statementKey) => { if (s[statementKey]) { coveredStatements += 1 } }) const hasStatements = totalStatements > 0 const allCovered = coveredStatements === totalStatements const coverageStatus = hasStatements ? (allCovered ? '✅' : '⚠️') : '❓' debug( '%s %s statements covered %d/%d', coverageStatus, key, coveredStatements, totalStatements ) }) } const tasks = { /** * Clears accumulated code coverage information. * * Interactive mode with "cypress open" * - running a single spec or "Run all specs" needs to reset coverage * Headless mode with "cypress run" * - runs EACH spec separately, so we cannot reset the coverage * or we will lose the coverage from previous specs. */ resetCoverage({ isInteractive }) { if (isInteractive) { debug('reset code coverage in interactive mode') coverageMap = istanbul.createCoverageMap({}) saveCoverage(coverageMap) } /* Else: in headless mode, assume the coverage file was deleted before the `cypress run` command was called example: rm -rf .nyc_output || true */ return null }, /** * Combines coverage information from single test * with previously collected coverage. * * @param {string} sentCoverage Stringified coverage object sent by the test runner * @returns {null} Nothing is returned from this task */ combineCoverage(sentCoverage) { const coverage = JSON.parse(sentCoverage) debug('parsed sent coverage') fixSourcePaths(coverage) coverageMap.merge(coverage) return null }, /** * Saves coverage information as a JSON file and calls * NPM script to generate HTML report */ coverageReport() { saveCoverage(coverageMap) if (!existsSync(nycFilename)) { console.warn('Cannot find coverage file %s', nycFilename) console.warn('Skipping coverage report') return null } showNycInfo(nycFilename) const allSourceFilesMissing = checkAllPathsNotFound(nycFilename) if (allSourceFilesMissing) { tryFindingLocalFiles(nycFilename) } resolveRelativePaths(nycFilename) if (customNycReportScript) { debug( 'saving coverage report using script "%s" from package.json, command: %s', DEFAULT_CUSTOM_COVERAGE_SCRIPT_NAME, customNycReportScript ) debug('current working directory is %s', process.cwd()) return execa('npm', ['run', DEFAULT_CUSTOM_COVERAGE_SCRIPT_NAME], { stdio: 'inherit' }) } if (nycReportOptions.all) { debug('nyc needs to report on all included files') includeAllFiles(nycFilename, nycReportOptions) } debug('calling NYC reporter with options %o', nycReportOptions) debug('current working directory is %s', process.cwd()) const NYC = require('nyc') const nyc = new NYC(nycReportOptions) const returnReportFolder = () => { const reportFolder = nycReportOptions['report-dir'] debug( 'after reporting, returning the report folder name %s', reportFolder ) maybePrintFinalCoverageFiles(reportFolder) return reportFolder } return nyc.report().then(returnReportFolder) } } /** * Registers code coverage collection and reporting tasks. * Sets an environment variable to tell the browser code that it can * send the coverage. * @example ``` // your plugins file module.exports = (on, config) => { require('cypress/code-coverage/task')(on, config) // IMPORTANT to return the config object // with the any changed environment variables return config } ``` */ function registerCodeCoverageTasks(on, config) { on('task', tasks) // set a variable to let the hooks running in the browser // know that they can send coverage commands config.env.codeCoverageTasksRegistered = true return config } module.exports = registerCodeCoverageTasks