├── 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 | 
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 |
--------------------------------------------------------------------------------