Created
December 16, 2018 08:34
-
-
Save NicholasBoll/c1ee8ddd0c3749db7480e0f84e851a30 to your computer and use it in GitHub Desktop.
Cypress mochawesome report. All files go into `cypress` directory
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const addContext = (report, screenshots, videoUrl) => { | |
const getTests = t => t.tests | |
const getSuites = t => t.suites | |
const addSuiteContext = (suite, previousTitles = []) => { | |
const titles = suite.title ? previousTitles.concat(suite.title) : previousTitles | |
getTests(suite).forEach(test => { | |
test.timedOut = false // for some reason this is dropped | |
const context = [ | |
{ | |
title: 'Video', | |
value: videoUrl, | |
}, | |
] | |
const testFileName = titles | |
.concat(test.title) | |
.join(' -- ') | |
.replace(/,/g, '') | |
const testScreenshots = screenshots.filter(s => s.includes(testFileName)) | |
testScreenshots.forEach(screenshot => { | |
// There could be multiple screenshots for various reasons. `${testFileName}.png` is the failure one. Others are postfixed with a name | |
if (screenshot.includes(`${testFileName}.png`)) { | |
context.splice(0, 0, { | |
title: 'Failure Screenshot', | |
value: screenshot, | |
}) | |
} else { | |
context.splice(0, 0, { | |
title: screenshot.match(`${testFileName}(.+).png`)[1].replace(' -- ', ''), | |
value: screenshot, | |
}) | |
} | |
}) | |
test.context = JSON.stringify(context) | |
}) | |
getSuites(suite).forEach(s => { | |
addSuiteContext(s, titles) | |
}) | |
} | |
addSuiteContext(report.suites) | |
return report | |
} | |
module.exports = addContext |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function getPercentClass(pct) { | |
if (pct <= 50) { | |
return 'danger' | |
} else if (pct > 50 && pct < 80) { | |
return 'warning' | |
} else { | |
return 'success' | |
} | |
} | |
const min = (a, b) => (a < b ? a : b) | |
const max = (a, b) => (a > b ? a : b) | |
function mergedReports(reports) { | |
const baseStats = { | |
suites: 0, | |
tests: 0, | |
passes: 0, | |
pending: 0, | |
failures: 0, | |
start: '2050-12-31T00:00:00.000Z', | |
end: '1940-01-01T00:00:00.000Z', | |
duration: 0, | |
testsRegistered: 0, | |
passPercent: 0, | |
pendingPercent: 0, | |
other: 0, | |
hasOther: false, | |
skipped: 0, | |
hasSkipped: false, | |
passPercentClass: 'success', | |
pendingPercentClass: 'success', | |
} | |
const mergeStats = (result, stat) => ({ | |
suites: result.suites + stat.suites, | |
tests: result.tests + stat.tests, | |
passes: result.passes + stat.passes, | |
pending: result.pending + stat.pending, | |
failures: result.failures + stat.failures, | |
start: min(result.start, stat.start), | |
end: max(result.end, stat.end), | |
duration: result.duration + stat.duration, | |
testsRegistered: result.testsRegistered + stat.testsRegistered, | |
other: result.other + stat.other, | |
hasOther: result.hasOther || stat.hasOther, | |
skipped: result.skipped + stat.skipped, | |
hasSkipped: result.hasSkipped || stat.hasSkipped, | |
}) | |
const stats = reports.map(f => f.stats).reduce(mergeStats, baseStats) | |
// calculated stats | |
stats.passPercent = Math.round(stats.passes / stats.tests * 100) | |
stats.pendingPercent = Math.round(stats.pending / stats.tests * 100) | |
stats.passPercentClass = getPercentClass(stats.passPercent) | |
stats.pendingPercentClass = getPercentClass(stats.pendingPercent) | |
/** Combine fields by merging the arrays */ | |
const concatFields = field => (result, item) => result.concat(item[field]) | |
const baseSuites = reports[0].suites | |
const suites = reports.reduce(concatFields('suites'), []) | |
const mergedReport = { | |
stats, | |
suites: Object.assign({}, baseSuites, { suites }), | |
copyrightYear: new Date().getFullYear(), | |
} | |
return mergedReport | |
} | |
module.exports = mergedReports |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const Rx = require('rxjs') | |
const cypress = require('cypress') | |
const path = require('path') | |
const glob = require('glob') | |
const fs = require('fs-extra') | |
const addContext = require('./addContext') | |
const mergeReports = require('./mergeReports') | |
const chalk = require('chalk') | |
const { argv } = require('yargs') | |
const cypressConfig = require('../cypress.json') | |
const marge = require('mochawesome-report-generator') | |
const flatten = a => [].concat(...a) | |
const getVideoPath = filePath => | |
path.resolve(__dirname, `videos_${path.basename(filePath, path.extname(filePath))}`) | |
const getScreenshotPath = filePath => | |
path.resolve(__dirname, `screenshots_${path.basename(filePath, path.extname(filePath))}`) | |
const files = flatten( | |
(argv._.length ? argv._ : ['**/*']).map(f => glob.sync(path.resolve(__dirname, 'integration', f))) | |
).filter(f => /\.[a-z]{1,6}$/.test(f)) | |
const assetPath = argv.assetPath || '' | |
const baseUrl = argv.baseUrl || cypressConfig.baseUrl | |
const usersFileName = argv.usersFileName || 'users.json' | |
const concurrency = parseInt(argv.concurrency, 10) || 1 | |
const retries = parseInt(argv.retries, 10) || 0 | |
if (files.length === 0) { | |
console.error(chalk.bold.red('No test files found')) | |
process.exit(1) | |
} | |
console.log('Running test files:') | |
console.log(files.map(f => path.relative(__dirname, f)).join('\n')) | |
// used to round-robin users | |
let testNumber = -1 | |
const getTestNumber = () => { | |
return testNumber++ | |
} | |
const getReporterOptions = filename => ({ | |
reportDir: './cypress/reports', | |
reportFilename: filename, | |
reportTitle: filename, | |
reportPageTitle: filename, | |
overwrite: true, | |
inline: true, | |
html: false, | |
json: true, | |
}) | |
const getConfig = file => ({ | |
spec: file, | |
config: { | |
videosFolder: getVideoPath(file), | |
screenshotsFolder: getScreenshotPath(file), | |
baseUrl, | |
}, | |
env: { | |
TEST_NUMBER: getTestNumber(), | |
USERS_FILE_NAME: usersFileName, | |
}, | |
reporter: 'mochawesome', | |
reporterOptions: getReporterOptions(path.basename(file)), | |
}) | |
fs.removeSync(path.resolve(__dirname, 'reports')) | |
fs.mkdirsSync(path.resolve(__dirname, 'reports', 'videos')) | |
fs.mkdirsSync(path.resolve(__dirname, 'reports', 'screenshots')) | |
const cypressRun = file => { | |
return cypress.run(getConfig(file)).then(results => { | |
if (results.totalTests === undefined) { | |
// no results were run - probably an error messages | |
throw results | |
} | |
const testName = path.basename(file, path.extname(file)) | |
let screenshots = [] | |
if (fs.pathExistsSync(getScreenshotPath(file))) { | |
fs.copySync(getScreenshotPath(file), path.resolve(__dirname, 'reports', 'screenshots')) | |
screenshots = glob | |
.sync(`${getScreenshotPath(file)}/**/*.png`, { | |
cwd: getScreenshotPath(file), | |
}) | |
.map(s => s.replace(getScreenshotPath(file), 'screenshots')) | |
fs.removeSync(getScreenshotPath(file)) | |
} | |
const video = glob.sync(`${getVideoPath(file)}/**/*.mp4`)[0] | |
fs.copySync(video, path.resolve(__dirname, 'reports', 'videos', `${testName}.mp4`)) | |
fs.removeSync(getVideoPath(file)) | |
const json = addContext( | |
JSON.parse(fs.readFileSync(path.resolve(__dirname, 'reports', `${testName}.json`))), | |
screenshots, | |
`${assetPath}videos/${testName}.mp4` | |
) | |
if (json.suites) { | |
json.suites.title = path.relative(`${__dirname}/integration`, file) | |
} | |
if (json.stats.failures || json.stats.tests === 0 || json.stats.other) { | |
throw json | |
} | |
return json | |
}) | |
} | |
const runSpec = file => | |
Rx.Observable.defer(() => cypressRun(file)) | |
.retry(retries) | |
.catch(error => { | |
if (error.stats && (error.stats.failures || error.stats.other)) { | |
return Rx.Observable.of(error) | |
} else { | |
return Rx.Observable.throw(error) | |
} | |
}) | |
const combineReports = reports => { | |
const mergedReports = mergeReports(reports) | |
marge.create( | |
mergedReports, | |
Object.assign(getReporterOptions('UI Test Results'), { | |
saveJson: true, | |
reportFilename: 'index', | |
}) | |
) | |
if (mergedReports.stats.failures || mergedReports.stats.other) { | |
process.exitCode = 1 | |
console.log(chalk.bold.red('Exit Code:'), process.exitCode) | |
} | |
} | |
Rx.Observable.of(...files) | |
.flatMap(runSpec, null, concurrency) | |
.filter(r => r) // only process good results | |
.toArray() | |
.subscribe({ | |
next: combineReports, | |
error: err => { | |
console.error(chalk.bold.red('Processing Error:'), err) | |
process.exitCode = 1 | |
}, | |
}) |
Copy the files into /cypress. Once inside, run 'node cypress/runner <spec_pattern>'. Hope this helps.
Very helpful tool, especially when understood.
Getting an error Rx.Observable.of(...files)
^
TypeError: Rx.Observable.of is not a function
at Object. (/cypress/runner.js:137:15)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
How do you use this?