Skip to content

Instantly share code, notes, and snippets.

@NicholasBoll
Created December 16, 2018 08:34
Show Gist options
  • Select an option

  • Save NicholasBoll/c1ee8ddd0c3749db7480e0f84e851a30 to your computer and use it in GitHub Desktop.

Select an option

Save NicholasBoll/c1ee8ddd0c3749db7480e0f84e851a30 to your computer and use it in GitHub Desktop.

Revisions

  1. NicholasBoll created this gist Dec 16, 2018.
    50 changes: 50 additions & 0 deletions addContext.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,50 @@
    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
    74 changes: 74 additions & 0 deletions mergeReports.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,74 @@
    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
    147 changes: 147 additions & 0 deletions runner.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,147 @@
    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
    },
    })