├── cypress.json ├── cypress ├── support │ └── index.js ├── integration │ └── spec.js └── plugins │ └── index.js ├── .gitignore ├── index.html ├── images └── coverage.png ├── app.js ├── package.json ├── convert2.js ├── README.md └── convert-coverage.js /cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .v8-coverage 3 | .nyc_output 4 | coverage 5 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 |

Hello world

3 | 4 | 5 | -------------------------------------------------------------------------------- /images/coverage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/cypress-native-chrome-code-coverage-example/HEAD/images/coverage.png -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | function add (a, b) { 2 | console.log('in function add') 3 | return a + b 4 | } 5 | 6 | function sub (a, b) { 7 | console.log('in function sub') 8 | return a - b 9 | } 10 | 11 | // call add twice to check code coverage counter 12 | console.log('adding 2 + 3 %d, 1 - 10 %d', add(2, 3), add(1, -10)) 13 | -------------------------------------------------------------------------------- /cypress/integration/spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | beforeEach(() => { 4 | console.log('before test') 5 | cy.task('beforeTest') 6 | }) 7 | 8 | afterEach(() => { 9 | console.log('after test') 10 | cy.task('afterTest') 11 | }) 12 | 13 | it('adds numbers', () => { 14 | // cy.wait(1000) 15 | cy.visit('/') 16 | cy.contains('Hello world').should('be.visible') 17 | // cy.wait(1000) 18 | }) 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-native-chrome-code-coverage-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "cy:open": "cypress open", 9 | "convert": "node ./convert-coverage", 10 | "report": "nyc report --reporter=html" 11 | }, 12 | "keywords": [], 13 | "author": "Gleb Bahmutov (https://glebbahmutov.com/)", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "chrome-remote-interface": "0.28.1", 17 | "cypress": "4.2.0", 18 | "mkdirp": "0.5.1", 19 | "nyc": "15.0.0", 20 | "puppeteer-to-istanbul": "1.2.2", 21 | "serve": "11.3.0", 22 | "v8-to-istanbul": "4.1.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /convert2.js: -------------------------------------------------------------------------------- 1 | const v8toIstanbul = require('v8-to-istanbul') 2 | 3 | const convertToIstanbul = async () => { 4 | // the path to the original source-file is required, as its contents are 5 | // used during the conversion algorithm. 6 | const converter = v8toIstanbul('./app.js') 7 | await converter.load() // this is required due to the async source-map dependency. 8 | // provide an array of coverage information in v8 format. 9 | 10 | const c8coverage = require('./.v8-coverage/coverage.json') 11 | const appCoverage = c8coverage.result[0].functions 12 | converter.applyCoverage(appCoverage) 13 | 14 | // output coverage information in a form that can 15 | // be consumed by Istanbul. 16 | console.info(JSON.stringify(converter.toIstanbul(), null, 2)) 17 | } 18 | 19 | convertToIstanbul().catch(err => { 20 | console.error(err) 21 | process.exit(1) 22 | }) 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cypress-native-chrome-code-coverage-example 2 | > Native code coverage in Chrome browser via Debugger protocol during Cypress end-to-end tests 3 | 4 | ## Main pieces 5 | 6 | - Connecting to Chrome via [chrome-remote-interface](https://github.com/cyrus-and/chrome-remote-interface) copied from [flotwig/cypress-log-to-output](https://github.com/flotwig/cypress-log-to-output). 7 | - Code coverage via [Debugger Protocol](https://chromedevtools.github.io/devtools-protocol/tot/Profiler/) copied from [https://github.com/cyrus-and/chrome-remote-interface/issues/170](https://github.com/cyrus-and/chrome-remote-interface/issues/170). See [cypress/plugins/index.js](cypress/plugins/index.js) 8 | - Conversion of v8 coverage to Istanbul-compatible coverage using [v8-to-istanbul](https://github.com/istanbuljs/v8-to-istanbul) 9 | 10 | ```shell 11 | npm run cy:open 12 | npm run report 13 | ``` 14 | 15 | The output is saved to `coverage/index.html` 16 | 17 | ![Coverage](images/coverage.png) 18 | 19 | ## More info 20 | 21 | - [A quick look at how Chrome's JavaScript code coverage feature works](https://www.mattzeunert.com/2017/03/29/how-does-chrome-code-coverage-work.html) 22 | - Istanbul [coverage format](https://github.com/gotwarlost/istanbul/blob/master/coverage.json.md) 23 | -------------------------------------------------------------------------------- /convert-coverage.js: -------------------------------------------------------------------------------- 1 | const v8ToIstanbul = require('v8-to-istanbul') 2 | const url = require('url') 3 | const path = require('path') 4 | const fs = require('fs') 5 | const mkdirp = require('mkdirp') 6 | 7 | const v8CoverageFilename = path.join(__dirname, '.v8-coverage', 'coverage.json') 8 | const v8Coverage = JSON.parse(fs.readFileSync(v8CoverageFilename)) 9 | const istanbulCoverageFolder = path.join(__dirname, '.nyc_output') 10 | // console.log(v8Coverage) 11 | 12 | if (!fs.existsSync(istanbulCoverageFolder)) { 13 | mkdirp(istanbulCoverageFolder) 14 | } 15 | 16 | v8Coverage.result.forEach(async report => { 17 | // console.log('report is %o', report) 18 | const u = new url.URL(report.url) 19 | const filename = path.join(__dirname, u.pathname) 20 | console.log('instrumenting file %s for url %s', filename, report.url) 21 | const converter = v8ToIstanbul(filename) 22 | // I wonder if this maps the source if there is a source map?! 23 | await converter.load() 24 | converter.applyCoverage(report.functions) 25 | 26 | const coverageFilename = path.join(istanbulCoverageFolder, 'out.json') 27 | const istanbulCoverage = converter.toIstanbul() 28 | fs.writeFileSync( 29 | coverageFilename, 30 | JSON.stringify(istanbulCoverage, null, 2) + '\n' 31 | ) 32 | }) 33 | 34 | // return Promise.all( 35 | // result.result.map(report => { 36 | // // need to map report from url to original file 37 | // // TODO probably we can fetch the script and save it as temp file 38 | // // or remap it to the original files using source maps 39 | // const u = new url.URL(report.url) 40 | // const filename = fromRoot(u.pathname) 41 | // console.log( 42 | // 'instrumenting file %s for url %s', 43 | // filename, 44 | // report.url 45 | // ) 46 | // const converter = v8ToIstanbul(filename) 47 | 48 | // return converter.load().then(() => { 49 | // console.log('%o', converter) 50 | // // console.log('applying coverage') 51 | // // console.log('%o', report.functions) 52 | // // reportFormatted.applyCoverage(report.functions) 53 | // // return reportFormatted.toIstanbul() 54 | // }) 55 | // }) 56 | // ).then(() => { 57 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | const CDP = require('chrome-remote-interface') 2 | const v8ToIstanbul = require('v8-to-istanbul') 3 | const url = require('url') 4 | const path = require('path') 5 | const fs = require('fs') 6 | const mkdirp = require('mkdirp') 7 | 8 | const fromRoot = path.join.bind(null, __dirname, '..', '..') 9 | const v8CoverageFolder = fromRoot('.v8-coverage') 10 | const istanbulCoverageFolder = fromRoot('.nyc_output') 11 | 12 | function log (msg) { 13 | console.log(msg) 14 | } 15 | 16 | let cdp 17 | 18 | const makeFolder = () => { 19 | // if (!fs.existsSync(v8CoverageFolder)) { 20 | // mkdirp.sync(v8CoverageFolder) 21 | // } 22 | if (!fs.existsSync(istanbulCoverageFolder)) { 23 | console.log('making folder: %s', istanbulCoverageFolder) 24 | mkdirp.sync(istanbulCoverageFolder) 25 | } 26 | } 27 | 28 | const convertToIstanbul = async (jsFilename, functionsC8coverage) => { 29 | // the path to the original source-file is required, as its contents are 30 | // used during the conversion algorithm. 31 | const converter = v8ToIstanbul(jsFilename) 32 | await converter.load() // this is required due to the async source-map dependency. 33 | // provide an array of coverage information in v8 format. 34 | 35 | // const c8coverage = require('./.v8-coverage/coverage.json') 36 | // const appCoverage = c8coverage.result[0].functions 37 | converter.applyCoverage(functionsC8coverage) 38 | 39 | // output coverage information in a form that can 40 | // be consumed by Istanbul. 41 | // console.info(JSON.stringify(converter.toIstanbul(), null, 2)) 42 | return converter.toIstanbul() 43 | } 44 | 45 | function browserLaunchHandler (browser, launchOptions) { 46 | console.log('browser is', browser) 47 | if (browser.name !== 'chrome') { 48 | return log( 49 | ` Warning: An unsupported browser is used, output will not be logged to console: ${ 50 | browser.name 51 | }` 52 | ) 53 | } 54 | 55 | // find how Cypress is going to control Chrome browser 56 | const rdpArgument = launchOptions.args.find(arg => arg.startsWith('--remote-debugging-port')) 57 | if (!rdpArgument) { 58 | return log(`Could not find launch argument that starts with --remote-debugging-port`) 59 | } 60 | const rdp = parseInt(rdpArgument.split('=')[1]) 61 | 62 | // and use this port ourselves too 63 | log(` Attempting to connect to Chrome Debugging Protocol on port ${rdp}`) 64 | 65 | const tryConnect = () => { 66 | new CDP({ 67 | port: rdp 68 | }) 69 | .then(_cdp => { 70 | cdp = _cdp 71 | log(' Connected to Chrome Debugging Protocol') 72 | 73 | /** captures logs from the browser */ 74 | // cdp.Log.enable() 75 | // cdp.Log.entryAdded(logEntry) 76 | 77 | /** captures logs from console.X calls */ 78 | // cdp.Runtime.enable() 79 | // cdp.Runtime.consoleAPICalled(logConsole) 80 | 81 | cdp.on('disconnect', () => { 82 | log(' Chrome Debugging Protocol disconnected') 83 | cdp = null 84 | }) 85 | }) 86 | .catch(() => { 87 | setTimeout(tryConnect, 100) 88 | }) 89 | } 90 | 91 | tryConnect() 92 | } 93 | 94 | module.exports = (on, config) => { 95 | // `on` is used to hook into various events Cypress emits 96 | // `config` is the resolved Cypress config 97 | on('before:browser:launch', browserLaunchHandler) 98 | on('task', { 99 | beforeTest () { 100 | log('before test') 101 | 102 | if (cdp) { 103 | log('starting code coverage') 104 | const callCount = true 105 | const detailed = true 106 | 107 | return Promise.all([ 108 | cdp.Profiler.enable(), 109 | cdp.Profiler.startPreciseCoverage(callCount, detailed) 110 | ]) 111 | } 112 | 113 | return null 114 | }, 115 | 116 | afterTest () { 117 | log('after test') 118 | 119 | if (cdp) { 120 | log('stopping code coverage') 121 | return cdp.Profiler.takePreciseCoverage().then(c8coverage => { 122 | // slice out unwanted scripts (like Cypress own specs) 123 | // minimatch would be better? 124 | const appFiles = /app\.js$/ 125 | 126 | // for now just grab results for "app.js" 127 | const appC8coverage = c8coverage.result.find(script => { 128 | return appFiles.test(script.url) 129 | }) 130 | // console.log(appC8coverage) 131 | return convertToIstanbul('./app.js', appC8coverage.functions) 132 | .then((istanbulCoverage) => { 133 | // result.result = result.result.filter(script => 134 | // appFiles.test(script.url) 135 | // ) 136 | 137 | makeFolder() 138 | 139 | const filename = path.join(istanbulCoverageFolder, 'out.json') 140 | const str = JSON.stringify(istanbulCoverage, null, 2) + '\n' 141 | fs.writeFileSync(filename, str, 'utf8') 142 | 143 | // const filename = path.join(v8CoverageFolder, 'coverage.json') 144 | // fs.writeFileSync(filename, JSON.stringify(result, null, 2) + '\n') 145 | 146 | // const istanbulReports = 147 | // pti.write(result) 148 | // console.log('%o', appScripts[0].functions) 149 | // console.log('%o', istanbulReports) 150 | 151 | return cdp.Profiler.stopPreciseCoverage() 152 | }) 153 | }) 154 | } 155 | 156 | return null 157 | } 158 | }) 159 | } 160 | --------------------------------------------------------------------------------