Skip to content

Instantly share code, notes, and snippets.

@NicholasBoll
Created December 16, 2018 08:34
Show Gist options
  • Save NicholasBoll/c1ee8ddd0c3749db7480e0f84e851a30 to your computer and use it in GitHub Desktop.
Save NicholasBoll/c1ee8ddd0c3749db7480e0f84e851a30 to your computer and use it in GitHub Desktop.
Cypress mochawesome report. All files go into `cypress` directory
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
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
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
},
})
@NilukaSripalim
Copy link

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