diff --git a/.circleci/config.yml b/.circleci/config.yml index 58e7a99c..bccc2604 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -75,6 +75,7 @@ workflows: - run: npx nyc report --check-coverage true --lines 100 --include cypress/unit.js - cypress/run: + # TODO switch to separate example in "examples/..." name: backend coverage requires: - cypress/install @@ -96,7 +97,7 @@ workflows: path: coverage # print code coverage summary to the terminal # and make sure there the coverage is above certain limit - - run: npx nyc report --check-coverage true --lines 85 + - run: npx nyc report --check-coverage true --lines 72 # and look at the server index file - should be fully covered - run: npx nyc report --check-coverage true --lines 100 --include test-backend/index.js @@ -280,6 +281,35 @@ workflows: node ../../scripts/only-covered main.js working_directory: examples/use-plugins-and-support + - cypress/run: + attach-workspace: true + name: example-one-spec + requires: + - cypress/install + # there are no jobs to follow this one + # so no need to save the workspace files (saves time) + no-workspace: true + command: npx cypress run --project examples/one-spec + # store screenshots and videos + store_artifacts: true + post-steps: + - run: cat examples/one-spec/.nyc_output/out.json + # store the created coverage report folder + # you can click on it in the CircleCI UI + # to see live static HTML site + - store_artifacts: + path: examples/one-spec/coverage + # make sure the examples captures 100% of code + - run: + command: npx nyc report --check-coverage true --lines 100 + working_directory: examples/one-spec + - run: + name: Check code coverage 📈 + command: | + node ../../scripts/check-coverage main.js + node ../../scripts/only-covered main.js + working_directory: examples/one-spec + - publish: filters: branches: @@ -296,3 +326,4 @@ workflows: - example-same-folder - example-support-files - example-use-plugins-and-support + - example-one-spec diff --git a/cypress/integration/filtering.js b/cypress/integration/filtering.js new file mode 100644 index 00000000..212c8618 --- /dev/null +++ b/cypress/integration/filtering.js @@ -0,0 +1,84 @@ +const { filterSpecsFromCoverage } = require('../../utils') + +describe('minimatch', () => { + it('string matches', () => { + expect( + Cypress.minimatch('/path/to/specA.js', '/path/to/specA.js'), + 'matches full strings' + ).to.be.true + + expect( + Cypress.minimatch('/path/to/specA.js', 'specA.js'), + 'does not match just the end' + ).to.be.false + + expect( + Cypress.minimatch('/path/to/specA.js', '**/specA.js'), + 'matches using **' + ).to.be.true + }) +}) + +describe('filtering specs', () => { + it('filters list of specs by single string', () => { + const config = cy.stub() + config.withArgs('testFiles').returns(['specA.js']) + config.withArgs('integrationFolder').returns('/path/to/integration/') + + const totalCoverage = { + '/path/to/specA.js': {}, + '/path/to/specB.js': {} + } + const result = filterSpecsFromCoverage(totalCoverage, config) + expect(result).to.deep.equal({ + '/path/to/specB.js': {} + }) + }) + + it('filters list of specs by pattern', () => { + const config = cy.stub() + config.withArgs('testFiles').returns(['**/*B.js']) + config.withArgs('integrationFolder').returns('/path/to/integration/') + + const totalCoverage = { + '/path/to/specA.js': {}, + '/path/to/specB.js': {} + } + const result = filterSpecsFromCoverage(totalCoverage, config) + expect(result).to.deep.equal({ + '/path/to/specA.js': {} + }) + }) + + it('filters list of specs by pattern and single spec', () => { + const config = cy.stub() + config.withArgs('testFiles').returns(['**/*B.js', 'specA.js']) + config.withArgs('integrationFolder').returns('/path/to/integration/') + + const totalCoverage = { + '/path/to/specA.js': {}, + '/path/to/specB.js': {} + } + const result = filterSpecsFromCoverage(totalCoverage, config) + expect(result, 'all specs have been filtered out').to.deep.equal({}) + }) + + it('filters list of specs in integration folder', () => { + const config = cy.stub() + config.withArgs('testFiles').returns('**/*.*') // default pattern + config.withArgs('integrationFolder').returns('/path/to/integration/') + + const totalCoverage = { + '/path/to/specA.js': {}, + '/path/to/specB.js': {}, + // these files should be removed + '/path/to/integration/spec1.js': {}, + '/path/to/integration/spec2.js': {} + } + const result = filterSpecsFromCoverage(totalCoverage, config) + expect(result).to.deep.equal({ + '/path/to/specA.js': {}, + '/path/to/specB.js': {} + }) + }) +}) diff --git a/examples/one-spec/.babelrc b/examples/one-spec/.babelrc new file mode 100644 index 00000000..7a016cf8 --- /dev/null +++ b/examples/one-spec/.babelrc @@ -0,0 +1,3 @@ +{ + "plugins": ["istanbul"] +} diff --git a/examples/one-spec/README.md b/examples/one-spec/README.md new file mode 100644 index 00000000..8341c68a --- /dev/null +++ b/examples/one-spec/README.md @@ -0,0 +1,3 @@ +# example: one-spec + +Only running a single spec diff --git a/examples/one-spec/cypress.json b/examples/one-spec/cypress.json new file mode 100644 index 00000000..e5a25c6e --- /dev/null +++ b/examples/one-spec/cypress.json @@ -0,0 +1,5 @@ +{ + "fixturesFolder": false, + "pluginsFile": "cypress/plugins/index.js", + "testFiles": ["spec-a.js"] +} diff --git a/examples/one-spec/cypress/integration/spec-a.js b/examples/one-spec/cypress/integration/spec-a.js new file mode 100644 index 00000000..92b53c22 --- /dev/null +++ b/examples/one-spec/cypress/integration/spec-a.js @@ -0,0 +1,13 @@ +/// +it('spec a', () => { + cy.visit('index.html') + cy.contains('Page body') + + cy.window() + .invoke('add', 2, 3) + .should('equal', 5) + + cy.window() + .invoke('sub', 2, 3) + .should('equal', -1) +}) diff --git a/examples/one-spec/cypress/integration/spec-b.js b/examples/one-spec/cypress/integration/spec-b.js new file mode 100644 index 00000000..a9a644cd --- /dev/null +++ b/examples/one-spec/cypress/integration/spec-b.js @@ -0,0 +1,5 @@ +/// +it('spec b', () => { + // should not run + throw new Error('Spec b should not run') +}) diff --git a/examples/one-spec/cypress/plugins/index.js b/examples/one-spec/cypress/plugins/index.js new file mode 100644 index 00000000..b17c48db --- /dev/null +++ b/examples/one-spec/cypress/plugins/index.js @@ -0,0 +1,5 @@ +module.exports = (on, config) => { + require('../../../../task')(on, config) + on('file:preprocessor', require('../../../../use-babelrc')) + return config +} diff --git a/examples/one-spec/cypress/support/index.js b/examples/one-spec/cypress/support/index.js new file mode 100644 index 00000000..dd60efa2 --- /dev/null +++ b/examples/one-spec/cypress/support/index.js @@ -0,0 +1 @@ +import '../../../../support' diff --git a/examples/one-spec/index.html b/examples/one-spec/index.html new file mode 100644 index 00000000..b6691c8a --- /dev/null +++ b/examples/one-spec/index.html @@ -0,0 +1,4 @@ + + Page body + + diff --git a/examples/one-spec/main-instrumented.js b/examples/one-spec/main-instrumented.js new file mode 100644 index 00000000..0550e9bb --- /dev/null +++ b/examples/one-spec/main-instrumented.js @@ -0,0 +1,146 @@ +function cov_6k5v991cn() { + var path = 'main.js' + var hash = 'd384017ecd51a8d90283ba0dec593332209519de' + var global = new Function('return this')() + var gcv = '__coverage__' + var coverageData = { + path: 'main.js', + statementMap: { + '0': { + start: { + line: 1, + column: 0 + }, + end: { + line: 1, + column: 28 + } + }, + '1': { + start: { + line: 1, + column: 23 + }, + end: { + line: 1, + column: 28 + } + }, + '2': { + start: { + line: 3, + column: 0 + }, + end: { + line: 3, + column: 28 + } + }, + '3': { + start: { + line: 3, + column: 23 + }, + end: { + line: 3, + column: 28 + } + } + }, + fnMap: { + '0': { + name: '(anonymous_0)', + decl: { + start: { + line: 1, + column: 13 + }, + end: { + line: 1, + column: 14 + } + }, + loc: { + start: { + line: 1, + column: 23 + }, + end: { + line: 1, + column: 28 + } + }, + line: 1 + }, + '1': { + name: '(anonymous_1)', + decl: { + start: { + line: 3, + column: 13 + }, + end: { + line: 3, + column: 14 + } + }, + loc: { + start: { + line: 3, + column: 23 + }, + end: { + line: 3, + column: 28 + } + }, + line: 3 + } + }, + branchMap: {}, + s: { + '0': 0, + '1': 0, + '2': 0, + '3': 0 + }, + f: { + '0': 0, + '1': 0 + }, + b: {}, + _coverageSchema: '1a1c01bbd47fc00a2c39e90264f33305004495a9', + hash: 'd384017ecd51a8d90283ba0dec593332209519de' + } + var coverage = global[gcv] || (global[gcv] = {}) + + if (!coverage[path] || coverage[path].hash !== hash) { + coverage[path] = coverageData + } + + var actualCoverage = coverage[path] + + cov_6k5v991cn = function() { + return actualCoverage + } + + return actualCoverage +} + +cov_6k5v991cn() +cov_6k5v991cn().s[0]++ + +window.add = (a, b) => { + cov_6k5v991cn().f[0]++ + cov_6k5v991cn().s[1]++ + return a + b +} + +cov_6k5v991cn().s[2]++ + +window.sub = (a, b) => { + cov_6k5v991cn().f[1]++ + cov_6k5v991cn().s[3]++ + return a - b +} +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm1haW4uanMiXSwibmFtZXMiOlsid2luZG93IiwiYWRkIiwiYSIsImIiLCJzdWIiXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztBQUFBQSxNQUFNLENBQUNDLEdBQVAsR0FBYSxDQUFDQyxDQUFELEVBQUlDLENBQUosS0FBVTtBQUFBO0FBQUE7QUFBQSxTQUFBRCxDQUFDLEdBQUdDLENBQUo7QUFBSyxDQUE1Qjs7OztBQUVBSCxNQUFNLENBQUNJLEdBQVAsR0FBYSxDQUFDRixDQUFELEVBQUlDLENBQUosS0FBVTtBQUFBO0FBQUE7QUFBQSxTQUFBRCxDQUFDLEdBQUdDLENBQUo7QUFBSyxDQUE1QiIsInNvdXJjZXNDb250ZW50IjpbIndpbmRvdy5hZGQgPSAoYSwgYikgPT4gYSArIGJcblxud2luZG93LnN1YiA9IChhLCBiKSA9PiBhIC0gYlxuIl19 diff --git a/examples/one-spec/main.js b/examples/one-spec/main.js new file mode 100644 index 00000000..5dd69be2 --- /dev/null +++ b/examples/one-spec/main.js @@ -0,0 +1,3 @@ +window.add = (a, b) => a + b + +window.sub = (a, b) => a - b diff --git a/examples/one-spec/package.json b/examples/one-spec/package.json new file mode 100644 index 00000000..a3db5393 --- /dev/null +++ b/examples/one-spec/package.json @@ -0,0 +1,7 @@ +{ + "name": "example-one-spec", + "description": "Only running a single spec", + "scripts": { + "cy:open": "../../node_modules/.bin/cypress open" + } +} diff --git a/support.js b/support.js index fc2c052d..9351290f 100644 --- a/support.js +++ b/support.js @@ -1,4 +1,7 @@ /// +// @ts-check + +const { filterSpecsFromCoverage } = require('./utils') /** * Sends collected code coverage object to the backend code @@ -53,28 +56,6 @@ const filterSupportFilesFromCoverage = totalCoverage => { return coverage } -/** - * remove coverage for the spec files themselves, - * only keep "external" application source file coverage - */ -const filterSpecsFromCoverage = totalCoverage => { - const integrationFolder = Cypress.config('integrationFolder') - const testFilePattern = Cypress.config('testFiles') - const isUsingDefaultTestPattern = testFilePattern === '**/*.*' - - const isInIntegrationFolder = filename => - filename.startsWith(integrationFolder) - const isTestFile = filename => Cypress.minimatch(filename, testFilePattern) - - const isA = (fileCoverge, filename) => isInIntegrationFolder(filename) - const isB = (fileCoverge, filename) => isTestFile(filename) - - const isTestFileFilter = isUsingDefaultTestPattern ? isA : isB - - const coverage = Cypress._.omitBy(totalCoverage, isTestFileFilter) - return coverage -} - const registerHooks = () => { let windowCoverageObjects diff --git a/utils.js b/utils.js index ba778d76..75a43f84 100644 --- a/utils.js +++ b/utils.js @@ -1,19 +1,64 @@ -module.exports = { - /** - * Replace source-map's path by the corresponding absolute file path - * (coverage report wouldn't work with source-map path being relative - * or containing Webpack loaders and query parameters) - */ - fixSourcePathes(coverage) { - Object.values(coverage).forEach(file => { - const { path: absolutePath, inputSourceMap } = file - const fileName = /([^\/\\]+)$/.exec(absolutePath)[1] - if (!inputSourceMap || !fileName) return - - if (inputSourceMap.sourceRoot) inputSourceMap.sourceRoot = '' - inputSourceMap.sources = inputSourceMap.sources.map(source => - source.includes(fileName) ? absolutePath : source - ) - }) +/// + +/** + * remove coverage for the spec files themselves, + * only keep "external" application source file coverage + */ +const filterSpecsFromCoverage = (totalCoverage, config = Cypress.config) => { + const integrationFolder = config('integrationFolder') + const testFilePattern = config('testFiles') + + // test files chould be: + // wild card string "**/*.*" (default) + // wild card string "**/*spec.js" + // list of wild card strings or names ["**/*spec.js", "spec-one.js"] + const testFilePatterns = Array.isArray(testFilePattern) + ? testFilePattern + : [testFilePattern] + + const isUsingDefaultTestPattern = testFilePattern === '**/*.*' + + const isTestFile = filename => { + const matchedPattern = testFilePatterns.some(specPattern => + Cypress.minimatch(filename, specPattern) + ) + const matchedEndOfPath = testFilePatterns.some(specPattern => + filename.endsWith(specPattern) + ) + return matchedPattern || matchedEndOfPath } + + const isInIntegrationFolder = filename => + filename.startsWith(integrationFolder) + + const isA = (fileCoverge, filename) => isInIntegrationFolder(filename) + const isB = (fileCoverge, filename) => isTestFile(filename) + + const isTestFileFilter = isUsingDefaultTestPattern ? isA : isB + + const coverage = Cypress._.omitBy(totalCoverage, isTestFileFilter) + return coverage +} + +/** + * Replace source-map's path by the corresponding absolute file path + * (coverage report wouldn't work with source-map path being relative + * or containing Webpack loaders and query parameters) + */ +function fixSourcePathes(coverage) { + Object.values(coverage).forEach(file => { + const { path: absolutePath, inputSourceMap } = file + const fileName = /([^\/\\]+)$/.exec(absolutePath)[1] + if (!inputSourceMap || !fileName) return + + if (inputSourceMap.sourceRoot) inputSourceMap.sourceRoot = '' + inputSourceMap.sources = inputSourceMap.sources.map(source => + source.includes(fileName) ? absolutePath : source + ) + }) +} + +module.exports = { + fixSourcePathes, + filterSpecsFromCoverage }