Skip to content

WIP keeping code coverage per individual test #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cypress/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ if (true) {
} else {
console.error('never reached')
}

document.getElementById('click').addEventListener('click', () => {
console.log('clicked')
})
1 change: 1 addition & 0 deletions cypress/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<body>
<h2>Test page</h2>
<p>Open the DevTools to see console messages</p>
<button id="click">click me</button>
<script src="app.js"></script>
</body>
5 changes: 5 additions & 0 deletions cypress/integration/spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,9 @@ context('Page test', () => {
.should('have.been.calledOnce')
.should('have.been.calledWith', 'just names', ['joe', 'mary'])
})

it('clicks on the button', function () {
cy.get('#click').click()
cy.get('@log').should('have.been.calledWith', 'clicked')
})
})
2 changes: 2 additions & 0 deletions cypress/plugins/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require('../../plugins')

module.exports = (on, config) => {
on('task', require('../../task'))
}
394 changes: 93 additions & 301 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,11 @@
"parcel-bundler": "1.12.3",
"start-server-and-test": "1.9.0",
"semantic-release": "15.13.12"
},
"dependencies": {
"chokidar": "2.1.6",
"globby": "9.2.0",
"its-name": "1.0.0",
"ws": "6.2.1"
}
}
93 changes: 93 additions & 0 deletions plugins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
const WebSocket = require('ws')
const path = require('path')
const fs = require('fs')
const globby = require('globby')

// https://github.com/websockets/ws#simple-server
// create socket even if not watching files to avoid
// tripping up client trying to connect
const wss = new WebSocket.Server({ port: 8765 })
let client // future Cypress client

// watch files using chokidar
const chokidar = require('chokidar')
// const cypressJson = require(join(process.cwd(), 'cypress.json'))
// const options = cypressJson['cypress-watch-and-reload']
const options = {
watch: 'cypress/*.js'
}

// load initial source code for app files
const sources = new Map()
const sourceFilenames = globby
.sync(options.watch, { cwd: process.cwd() })
.map(s => path.resolve(process.cwd(), s))
sourceFilenames.forEach(filename => {
sources[filename] = fs.readFileSync(filename, 'utf8')
})

const compareSources = (before, after) => {
if (!before) {
console.log('missing before source')
return
}
if (!after) {
console.log('missing after source')
return
}
// only detect changed lines for now
const beforeLines = before.split('\n')
const afterLines = after.split('\n')
if (beforeLines.length !== afterLines.length) {
console.log('source has changed length')
return
}
const changed = []
beforeLines.forEach((s, k) => {
if (s !== afterLines[k]) {
console.log('line %d changed: %s', k, s)
changed.push(k)
}
})
return changed
}

if (options && typeof options.watch === 'string') {
console.log('will watch "%s"', options.watch)

wss.on('connection', function connection (ws) {
console.log('new socket connection 🎉')
client = ws

console.log('starting to watch file index.html')
// TODO clear previous watcher
chokidar.watch(options.watch).on('change', (relativePath, event) => {
console.log('file %s has changed', relativePath)
const filename = path.resolve(process.cwd(), relativePath)
const source = fs.readFileSync(filename, 'utf8')

const changedLines = compareSources(sources[filename], source)
// update our cached sourced
sources[filename] = source

if (changedLines && changedLines.length) {
console.log('Changed lines', changedLines)
}

if (client) {
const text = JSON.stringify({
command: 'changed',
filename,
source,
changedLines
})
client.send(text)
}
})
})
} else {
console.log(
'nothing to watch. Use cypress.json to set "cypress-watch-and-reload" object'
)
console.log('see https://github.com/bahmutov/cypress-watch-and-reload#use')
}
103 changes: 101 additions & 2 deletions support.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,39 @@
/// <reference types="cypress" />
const itsName = require('its-name')

let codeCoveragePerTest = new Map()
// let sources = new Map()

before(() => {
// we need to reset the coverage when running
// in the interactive mode, otherwise the counters will
// keep increasing every time we rerun the tests
cy.task('resetCoverage', { isInteractive: Cypress.config('isInteractive') })
const isInteractive = Cypress.config('isInteractive')

cy.task('resetCoverage', { isInteractive })

if (isInteractive) {
codeCoveragePerTest = new Map()
}
})

afterEach(() => {
// use "function () {...}" callback to get "this" pointing
// at the right context
afterEach(function () {
// save coverage after each test
// because the entire "window" object is about
// to be recycled by Cypress before next test
cy.window().then(win => {
if (win.__coverage__) {
// only keep track of code coverage per test in the interactive mode
if (Cypress.config('isInteractive')) {
// testName will be an array of strings
const testName = itsName(this.currentTest)
console.log('test that finished', testName)

codeCoveragePerTest[testName] = win.__coverage__
}

cy.task('combineCoverage', win.__coverage__)
}
})
Expand All @@ -20,4 +42,81 @@ afterEach(() => {
after(() => {
// when all tests finish, lets generate the coverage report
cy.task('coverageReport')

console.log(
'after all tests finished, coverage per test is',
codeCoveragePerTest
)
})

const ws = new WebSocket('ws://localhost:8765')

beforeEach(() => {
assert(ws.readyState === WebSocket.OPEN, 'Watch & Reload is ready')

const onChangedApplicationFile = (filename, source, changedLines) => {
console.log('file "%s" has changed', filename)
// console.log('new source')
// console.log(source)
console.log('changed lines', changedLines)

// find all tests that cover each changed line
const testsToRun = []
Object.entries(codeCoveragePerTest).forEach(([testName, coverage]) => {
// console.log('test name', testName)
// console.log('coverage', coverage)
const fileCoverage = coverage[filename]
if (!fileCoverage) {
return
}
// note that statement map starts at line 1
console.log(fileCoverage)
const testCoversLine = Object.values(fileCoverage.statementMap).some(
(s, k) => {
if (!fileCoverage.s[k]) {
return // the statement is NOT hit by the test
}
return changedLines.some(line => {
return s.start.line === line + 1 && s.end.line === line + 1
})
}
)
if (testCoversLine) {
console.log(
'test',
testName,
'covers one of the changed lines',
changedLines,
'in file',
filename
)
testsToRun.push(testName)
}
})

if (testsToRun.length) {
console.log('only running tests', testsToRun)
}
}

const fn = Cypress._.debounce(onChangedApplicationFile, 2000)

ws.onmessage = ev => {
console.log('message from OS')
console.log(ev)
if (ev.type === 'message' && ev.data) {
try {
const data = JSON.parse(ev.data)
if (data.command === 'changed' && data.filename && data.source) {
fn(data.filename, data.source, data.changedLines)
// window.top.document.querySelector('.reporter .restart').click()
}
} catch (e) {
console.error('Could not parse message from plugin')
console.error(e.message)
console.error('original text')
console.error(ev.data)
}
}
}
})