diff --git a/bin/cypress b/bin/cypress index 5344967f..40cd3705 100755 --- a/bin/cypress +++ b/bin/cypress @@ -1,5 +1,33 @@ #!/usr/bin/env node -const { cypressRecorder } = require('../lib/cypress-recorder'); +const fs = require('fs'); +const path = require('path'); +const stream = require('stream'); +const childProcess = require('child_process'); -(async () => await cypressRecorder())(); +const fd = fs.openSync( + path.join(process.cwd(), 'console.log'), + 'w+', + 0o644 +); +const ws = new stream.Writable({ + write (data, encoding, cb) { + fs.write(fd, data, undefined, encoding, cb); + } +}); + +const [nodeBin] = process.argv; +const child = childProcess.spawn(nodeBin, [ + path.join(__dirname, 'lib', 'cypress-runner.js'), + ...process.argv.slice(2) +]); + +child.stdout.pipe(process.stdout); +child.stderr.pipe(process.stderr); +child.stdout.pipe(ws); +child.stderr.pipe(ws); + +child.on('exit', (exitCode) => { + fs.closeSync(fd); + process.exit(exitCode); +}); diff --git a/package-lock.json b/package-lock.json index e68799c6..4ec8fdcf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,8 @@ "@cypress/grep": "4.0.1", "@cypress/webpack-preprocessor": "6.0.1", "@cypress/xpath": "2.0.3", - "@saucelabs/cypress-plugin": "3.1.2", + "@saucelabs/cypress-junit-plugin": "0.2.0", + "@saucelabs/cypress-plugin": "3.1.3", "@shelex/cypress-allure-plugin": "2.40.1", "@tsconfig/node20": "20.1.2", "babel-loader": "9.1.3", @@ -29,7 +30,6 @@ "fluent-ffmpeg": "^2.1.2", "lodash": "4.17.21", "mkdirp": "^3.0.1", - "mocha-junit-reporter": "^2.2.1", "playwright-webkit": "1.41.0", "sauce-testrunner-utils": "2.0.0", "typescript": "5.3.3", @@ -3709,6 +3709,50 @@ "@octokit/openapi-types": "^19.0.2" } }, + "node_modules/@oozcitak/dom": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.10.tgz", + "integrity": "sha512-0JT29/LaxVgRcGKvHmSrUTEvZ8BXvZhGl2LASRUgHqDTC1M5g1pLmVv56IYNyt3bG2CUjDkc67wnyZC14pbQrQ==", + "dependencies": { + "@oozcitak/infra": "1.0.8", + "@oozcitak/url": "1.0.4", + "@oozcitak/util": "8.3.8" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@oozcitak/infra": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.8.tgz", + "integrity": "sha512-JRAUc9VR6IGHOL7OGF+yrvs0LO8SlqGnPAMqyzOuFZPSZSXI7Xf2O9+awQPSMXgIWGtgUf/dA6Hs6X6ySEaWTg==", + "dependencies": { + "@oozcitak/util": "8.3.8" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/@oozcitak/url": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.4.tgz", + "integrity": "sha512-kDcD8y+y3FCSOvnBI6HJgl00viO/nGbQoCINmQ0h98OhnGITrWR3bOGfwYCthgcrV8AnTJz8MzslTQbC3SOAmw==", + "dependencies": { + "@oozcitak/infra": "1.0.8", + "@oozcitak/util": "8.3.8" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@oozcitak/util": { + "version": "8.3.8", + "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.8.tgz", + "integrity": "sha512-T8TbSnGsxo6TDBJx/Sgv/BlVJL3tshxZP7Aq5R1mSnM5OcHY2dQaxLMu2+E8u3gN0MLOzdjurqN4ZRVuzQycOQ==", + "engines": { + "node": ">=8.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3771,10 +3815,24 @@ "node": ">=12" } }, + "node_modules/@saucelabs/cypress-junit-plugin": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@saucelabs/cypress-junit-plugin/-/cypress-junit-plugin-0.2.0.tgz", + "integrity": "sha512-eB7K4EYSiJmG7bIdiMsSht6GPjGGGQXEtBmX+2fA40UcrhcmzWj6a49c/HWCGFSxuTVhpGNyqXS0Ml1ArWqhvQ==", + "dependencies": { + "xmlbuilder2": "^3.1.1" + }, + "engines": { + "node": ">=16.13.2" + }, + "peerDependencies": { + "cypress": ">=13" + } + }, "node_modules/@saucelabs/cypress-plugin": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@saucelabs/cypress-plugin/-/cypress-plugin-3.1.2.tgz", - "integrity": "sha512-SLXXrdAqm70yqI8AkqLkAI1IsvC1TXdhqVNZvLy2ewwN4osW8hHAXcIK3gktGwFoEyvj/kvai71Iqc8XREYzwg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@saucelabs/cypress-plugin/-/cypress-plugin-3.1.3.tgz", + "integrity": "sha512-Z4O3GFAUKre9f0pb9b9WL5COrdAGo9FMPENmDLecD8K49hUH1dqVmWjLGWqS8eEaixJDa4TKJzCeAU8vzkOPKw==", "dependencies": { "@saucelabs/sauce-json-reporter": "^3.0.3", "@saucelabs/testcomposer": "^1.2.1", @@ -4867,7 +4925,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, "dependencies": { "sprintf-js": "~1.0.2" } @@ -5752,14 +5809,6 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, - "node_modules/charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", - "engines": { - "node": "*" - } - }, "node_modules/check-more-types": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", @@ -6256,14 +6305,6 @@ "node": ">= 8" } }, - "node_modules/crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", - "engines": { - "node": "*" - } - }, "node_modules/crypto-js": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", @@ -8055,7 +8096,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -9593,11 +9633,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -11441,7 +11476,6 @@ "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -11998,16 +12032,6 @@ "tmpl": "1.0.5" } }, - "node_modules/md5": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "dependencies": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -12152,21 +12176,6 @@ "url": "https://opencollective.com/mochajs" } }, - "node_modules/mocha-junit-reporter": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/mocha-junit-reporter/-/mocha-junit-reporter-2.2.1.tgz", - "integrity": "sha512-iDn2tlKHn8Vh8o4nCzcUVW4q7iXp7cC4EB78N0cDHIobLymyHNwe0XG8HEHHjc3hJlXm0Vy6zcrxaIhnI2fWmw==", - "dependencies": { - "debug": "^4.3.4", - "md5": "^2.3.0", - "mkdirp": "^3.0.0", - "strip-ansi": "^6.0.1", - "xml": "^1.0.1" - }, - "peerDependencies": { - "mocha": ">=2.2.5" - } - }, "node_modules/mocha/node_modules/ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -18228,8 +18237,7 @@ "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, "node_modules/sshpk": { "version": "1.17.0", @@ -19725,11 +19733,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/xml": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", - "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==" - }, "node_modules/xml-js": { "version": "1.6.11", "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", @@ -19749,6 +19752,20 @@ "node": ">=8.0" } }, + "node_modules/xmlbuilder2": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-3.1.1.tgz", + "integrity": "sha512-WCSfbfZnQDdLQLiMdGUQpMxxckeQ4oZNMNhLVkcekTu7xhD4tuUDyAPoY8CwXvBYE6LwBHd6QW2WZXlOWr1vCw==", + "dependencies": { + "@oozcitak/dom": "1.15.10", + "@oozcitak/infra": "1.0.8", + "@oozcitak/util": "8.3.8", + "js-yaml": "3.14.1" + }, + "engines": { + "node": ">=12.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index a532c3c5..7bf5aa64 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "release:patch": "npm run release -- patch", "release:minor": "npm run release -- minor", "release:major": "npm run release -- major", - "unit-test": "jest --env node" + "unit-test": "jest --env node --passWithNoTests" }, "keywords": [], "dependencies": { @@ -35,7 +35,8 @@ "@cypress/grep": "4.0.1", "@cypress/webpack-preprocessor": "6.0.1", "@cypress/xpath": "2.0.3", - "@saucelabs/cypress-plugin": "3.1.2", + "@saucelabs/cypress-junit-plugin": "0.2.0", + "@saucelabs/cypress-plugin": "3.1.3", "@shelex/cypress-allure-plugin": "2.40.1", "@tsconfig/node20": "20.1.2", "babel-loader": "9.1.3", @@ -47,7 +48,6 @@ "fluent-ffmpeg": "^2.1.2", "lodash": "4.17.21", "mkdirp": "^3.0.1", - "mocha-junit-reporter": "^2.2.1", "playwright-webkit": "1.41.0", "sauce-testrunner-utils": "2.0.0", "typescript": "5.3.3", diff --git a/src/custom-reporter.ts b/src/custom-reporter.ts deleted file mode 100644 index b36b912b..00000000 --- a/src/custom-reporter.ts +++ /dev/null @@ -1,661 +0,0 @@ -/** - * This is an extension of mocha-junit-reporter - */ - -import xml from 'xml'; -import Mocha from 'mocha'; -import fs from 'fs'; -import path from 'path'; -import Debug from 'debug'; -import { mkdirpSync } from 'mkdirp'; -import md5 from 'md5'; -import stripAnsi from 'strip-ansi'; -import EventEmitter from 'events'; - -import { - XmlProperties, - XmlSuite, - XmlSuiteAttrContainer, - XmlSuiteAttributes, - XmlTestCaseContainer, -} from './types'; - -const Base = Mocha.reporters.Base; -const debug = Debug('mocha-junit-reporter'); - -let createStatsCollector: (arg0: any) => void; -let mocha6plus = false; - -try { - const json = JSON.parse( - fs.readFileSync( - path.dirname(require.resolve('mocha')) + '/package.json', - 'utf8', - ), - ); - const version = json.version; - if (version >= '6') { - createStatsCollector = require('mocha/lib/stats-collector'); - mocha6plus = true; - } else { - mocha6plus = false; - } -} catch (e) { - console.warn("Couldn't determine Mocha version"); -} - -// A subset of invalid characters as defined in http://www.w3.org/TR/xml/#charsets that can occur in e.g. stacktraces -// regex lifted from https://github.com/MylesBorins/xml-sanitizer/ (licensed MIT) -const INVALID_CHARACTERS_REGEX = - /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007f-\u0084\u0086-\u009f\uD800-\uDFFF\uFDD0-\uFDFF\uFFFF\uC008]/g; //eslint-disable-line no-control-regex - -function findReporterOptions(options: any): any { - debug('Checking for options in', options); - if (!options) { - debug('No options provided'); - return {}; - } - if (!mocha6plus) { - debug('Options for pre mocha@6'); - return options.reporterOptions || {}; - } - if (options.reporterOptions) { - debug('Command-line options for mocha@6+'); - return options.reporterOptions; - } - // this is require to handle .mocharc.js files - debug('Looking for .mocharc.js options'); - return Object.keys(options) - .filter(function (key) { - return key.indexOf('reporterOptions.') === 0; - }) - .reduce(function (reporterOptions, key) { - reporterOptions[key.substring('reporterOptions.'.length)] = options[key]; - return reporterOptions; - }, {}); -} - -function configureDefaults(options: any): any { - const config = findReporterOptions(options); - debug('options', config); - config.mochaFile = getSetting( - config.mochaFile, - 'MOCHA_FILE', - 'test-results.xml', - ); - config.attachments = getSetting(config.attachments, 'ATTACHMENTS', false); - config.antMode = getSetting(config.antMode, 'ANT_MODE', false); - config.jenkinsMode = getSetting(config.jenkinsMode, 'JENKINS_MODE', false); - config.properties = getSetting( - config.properties, - 'PROPERTIES', - null, - parsePropertiesFromEnv, - ); - config.toConsole = !!config.toConsole; - config.rootSuiteTitle = config.rootSuiteTitle || 'Root Suite'; - config.testsuitesTitle = config.testsuitesTitle || 'Mocha Tests'; - - if (config.antMode) { - updateOptionsForAntMode(config); - } - - if (config.jenkinsMode) { - updateOptionsForJenkinsMode(config); - } - - config.suiteTitleSeparatedBy = config.suiteTitleSeparatedBy || ' '; - - return config; -} - -function updateOptionsForAntMode(options: any) { - options.antHostname = getSetting( - options.antHostname, - 'ANT_HOSTNAME', - process.env.HOSTNAME, - ); - - if (!options.properties) { - options.properties = {}; - } -} - -function updateOptionsForJenkinsMode(options: any) { - if (options.useFullSuiteTitle === undefined) { - options.useFullSuiteTitle = true; - } - debug( - 'jenkins mode - testCaseSwitchClassnameAndName', - options.testCaseSwitchClassnameAndName, - ); - if (options.testCaseSwitchClassnameAndName === undefined) { - options.testCaseSwitchClassnameAndName = true; - } - if (options.suiteTitleSeparatedBy === undefined) { - options.suiteTitleSeparatedBy = '.'; - } -} - -/** - * Determine an option value. - * 1. If `key` is present in the environment, then use the environment value - * 2. If `value` is specified, then use that value - * 3. Fall back to `defaultVal` - * @module mocha-junit-reporter - * @param {Object} value - the value from the reporter options - * @param {String} key - the environment variable to check - * @param {Object} defaultVal - the fallback value - * @param {function} transform - a transformation function to be used when loading values from the environment - */ -function getSetting( - value: any, - key: string, - defaultVal: any, - transform: any | undefined = undefined, -) { - if (process.env[key] !== undefined) { - const envVal = process.env[key]; - return typeof transform === 'function' ? transform(envVal) : envVal; - } - if (value !== undefined) { - return value; - } - return defaultVal; -} - -function defaultSuiteTitle(suite: any) { - if (suite.root && suite.title === '') { - return stripAnsi(this._options.rootSuiteTitle); - } - return stripAnsi(suite.title); -} - -function fullSuiteTitle(suite: any) { - let parent = suite.parent; - const title = [suite.title]; - - while (parent) { - if (parent.root && parent.title === '') { - title.unshift(this._options.rootSuiteTitle); - } else { - title.unshift(parent.title); - } - parent = parent.parent; - } - - return stripAnsi(title.join(this._options.suiteTitleSeparatedBy)); -} - -function isInvalidSuite(suite: any) { - return ( - (!suite.root && suite.title === '') || - (suite.tests.length === 0 && suite.suites.length === 0) - ); -} - -function parsePropertiesFromEnv(envValue: string) { - if (envValue) { - debug('Parsing from env', envValue); - return envValue.split(',').reduce(function (properties, prop) { - const property = prop.split(':'); - properties[property[0]] = property[1]; - return properties; - }, []); - } - - return null; -} - -function generateProperties(options: any) { - const props = options.properties; - if (!props) { - return []; - } - return Object.keys(props).reduce(function (properties, name) { - const value = props[name]; - properties.push({ property: { _attr: { name, value } } }); - return properties; - }, []); -} - -function getJenkinsClassname(test: any, options: any) { - debug('Building jenkins classname for', test); - let parent = test.parent; - const titles = []; - while (parent) { - parent.title && titles.unshift(parent.title); - parent = parent.parent; - } - return titles.join(options.suiteTitleSeparatedBy); -} - -/** - * JUnit reporter for mocha.js. - */ -function MochaJUnitReporter(runner: EventEmitter, options: object) { - if (mocha6plus) { - createStatsCollector(runner); - } - this._options = configureDefaults(options); - this._runner = runner; - this._generateSuiteTitle = this._options.useFullSuiteTitle - ? fullSuiteTitle - : defaultSuiteTitle; - this._antId = 0; - const testsuites = []; - this._testsuites = testsuites; - const sauceJson = []; - this._sauceJson = sauceJson; - - function lastSuite() { - return testsuites[testsuites.length - 1].testsuite; - } - - // get functionality from the Base reporter - Base.call(this, runner); - - // remove old results - this._runner.on( - 'start', - function (/*d*/) { - if (fs.existsSync(this._options.mochaFile)) { - debug('removing report file', this._options.mochaFile); - fs.unlinkSync(this._options.mochaFile); - } - }.bind(this), - ); - - this._runner.on( - 'suite', - function (suite) { - if (!isInvalidSuite(suite)) { - if (suite.tests.length > 0) { - sauceJson.push(this.getSauceTestsuiteData(suite)); - } - testsuites.push(this.getTestsuiteData(suite)); - } - }.bind(this), - ); - - this._runner.on( - 'pass', - function (test) { - sauceJson.push(this.getSauceTestcaseData(test)); - lastSuite().push(this.getTestcaseData(test)); - }.bind(this), - ); - - this._runner.on( - 'fail', - function (test, err) { - if ( - (test.err && test.err.expected !== undefined) || - (test.err && test.err.actual !== undefined) - ) { - console.error('- expected: ', test.err && test.err.expected); - console.error('+ actual: ', test.err && test.err.actual); - } - console.error(test.err && test.err.codeFrame && test.err.codeFrame.frame); - console.error(test.err.message); - sauceJson.push(this.getSauceTestcaseData(test)); - lastSuite().push(this.getTestcaseData(test, err)); - }.bind(this), - ); - - if (this._options.includePending) { - this._runner.on( - 'pending', - function (test) { - const testcase = this.getTestcaseData(test); - - testcase.testcase.push({ skipped: null }); - lastSuite().push(testcase); - }.bind(this), - ); - } - - this._runner.on('end', () => { - this.report(testsuites, sauceJson); - }); -} - -MochaJUnitReporter.prototype.report = function ( - testsuites: any[], - sauceJson: any, -) { - if (this._runner.suite.file) { - const specFile = this._runner.suite.file; - const specRoot = this._options.specRoot; - this.flush(testsuites, path.relative(specRoot, specFile), sauceJson); - } -}; - -MochaJUnitReporter.prototype.getSauceTestsuiteData = function (suite: any) { - const _attr = { - name: this._generateSuiteTitle(suite), - timestamp: new Date().toISOString().slice(0, -5), - tests: suite.tests.length, - }; - return { - status: 'info', - message: _attr.name, - screenshot: null, - }; -}; - -MochaJUnitReporter.prototype.getSauceTestcaseData = function (testcase: any) { - return { - id: testcase.order, - screenshot: 0, - HTTPStatus: testcase.state === 'passed' ? 200 : 500, - suggestion: null, - statusCode: testcase.state === 'passed' ? 0 : 1, - path: testcase.title, - between_commands: testcase.duration, - result: { - status: testcase.state, - failure_reason: JSON.stringify({ - message: testcase.err && testcase.err.message, - stack: testcase.err && testcase.err.stack, - }), - }, - request: { - body: testcase.body, - }, - in_video_timeline: 0, - }; -}; -/** - * Produces an xml node for a test suite - * @param {Object} suite - a test suite - * @return {Object} - an object representing the xml node - */ -MochaJUnitReporter.prototype.getTestsuiteData = function (suite: any) { - const antMode = this._options.antMode; - - const _attr = { - name: this._generateSuiteTitle(suite), - timestamp: new Date().toISOString().slice(0, -5), - tests: suite.tests.length, - } as XmlSuiteAttributes; - const testSuite = { testsuite: [{ _attr }] } as XmlSuite; - - if (suite.file) { - (testSuite.testsuite[0] as XmlSuiteAttrContainer)._attr.file = suite.file; - } - - const properties = generateProperties(this._options); - if (properties.length || antMode) { - testSuite.testsuite.push({ - properties, - }); - } - - if (antMode) { - _attr.package = _attr.name; - _attr.hostname = this._options.antHostname; - _attr.id = this._antId; - _attr.errors = 0; - this._antId += 1; - } - - return testSuite; -}; - -/** - * Produces an xml config for a given test case. - */ -MochaJUnitReporter.prototype.getTestcaseData = function ( - test: any, - err: any, -): object { - const jenkinsMode = this._options.jenkinsMode; - const flipClassAndName = this._options.testCaseSwitchClassnameAndName; - const name = stripAnsi( - jenkinsMode ? getJenkinsClassname(test, this._options) : test.fullTitle(), - ); - const classname = stripAnsi(test.title); - const testcase = { - testcase: [ - { - _attr: { - name: flipClassAndName ? classname : name, - time: typeof test.duration === 'undefined' ? 0 : test.duration / 1000, - classname: flipClassAndName ? name : classname, - }, - }, - ], - } as XmlTestCaseContainer; - - // We need to merge console.logs and attachments into one - - // see JUnit schema (only accepts 1 per test). - let systemOutLines = []; - if ( - this._options.outputs && - test.consoleOutputs && - test.consoleOutputs.length > 0 - ) { - systemOutLines = systemOutLines.concat(test.consoleOutputs); - } - if ( - this._options.attachments && - test.attachments && - test.attachments.length > 0 - ) { - systemOutLines = systemOutLines.concat( - test.attachments.map(function (file) { - return '[[ATTACHMENT|' + file + ']]'; - }), - ); - } - if (systemOutLines.length > 0) { - testcase.testcase.push({ - 'system-out': this.removeInvalidCharacters( - stripAnsi(systemOutLines.join('\n')), - ), - }); - } - - if ( - this._options.outputs && - test.consoleErrors && - test.consoleErrors.length > 0 - ) { - testcase.testcase.push({ - 'system-err': this.removeInvalidCharacters( - stripAnsi(test.consoleErrors.join('\n')), - ), - }); - } - - if (err) { - let message; - if (err.message && typeof err.message.toString === 'function') { - message = err.message + ''; - } else if (typeof err.inspect === 'function') { - message = err.inspect() + ''; - } else { - message = ''; - } - const failureMessage = err.stack || message; - const failureElement = { - _attr: { - message: this.removeInvalidCharacters(message) || '', - type: err.name || '', - }, - _cdata: this.removeInvalidCharacters(failureMessage), - }; - - testcase.testcase.push({ failure: failureElement }); - } - return testcase; -}; - -MochaJUnitReporter.prototype.removeInvalidCharacters = function ( - input: string, -): string { - if (!input) { - return input; - } - return input.replace(INVALID_CHARACTERS_REGEX, ''); -}; - -/** - * Writes xml to disk and ouputs content if "toConsole" is set to true. - */ -MochaJUnitReporter.prototype.flush = function ( - testsuites: any[], - specFile: string, - sauceJson: any, -) { - this._xml = this.getXml(testsuites); - - this.writeXmlToDisk(this._xml, this._options.mochaFile, specFile); - - if (this._options.toConsole === true) { - console.log(this._xml); // eslint-disable-line no-console - } - - this.writeSauceJsonToDisk(sauceJson, this._options.mochaFile, specFile); -}; - -/** - * Produces an XML string from the given test data. - */ -MochaJUnitReporter.prototype.getXml = function ( - testsuites: XmlSuite[], -): string { - let totalSuitesTime = 0; - let totalTests = 0; - const stats = this._runner.stats; - const antMode = this._options.antMode; - const hasProperties = !!this._options.properties || antMode; - - testsuites.forEach(function (suite) { - const _suiteAttr = (suite.testsuite[0] as XmlSuiteAttrContainer)._attr; - // testsuite is an array: [attrs, properties?, testcase, testcase, …] - // we want to make sure that we are grabbing test cases at the correct index - const _casesIndex = hasProperties ? 2 : 1; - const _cases = suite.testsuite.slice(_casesIndex); - let missingProps; - - _suiteAttr.time = 0; - _suiteAttr.failures = 0; - _suiteAttr.skipped = 0; - - let suiteTime = 0; - _cases.forEach(function (testcase: XmlTestCaseContainer) { - const lastNode = testcase.testcase[testcase.testcase.length - 1]; - - _suiteAttr.skipped += Number('skipped' in lastNode); - _suiteAttr.failures += Number('failure' in lastNode); - suiteTime += testcase.testcase[0]._attr.time as number; - testcase.testcase[0]._attr.time = ( - testcase.testcase[0]._attr.time as number - ).toFixed(4); - }); - _suiteAttr.time = suiteTime.toFixed(4); - - if (antMode) { - missingProps = ['system-out', 'system-err']; - suite.testsuite.forEach(function (item) { - missingProps = missingProps.filter(function (prop) { - return !item[prop]; - }); - }); - missingProps.forEach(function (prop) { - const obj = {} as XmlProperties; - obj[prop] = []; - suite.testsuite.push(obj); - }); - } - - if (!_suiteAttr.skipped) { - delete _suiteAttr.skipped; - } - - totalSuitesTime += suiteTime; - totalTests += _suiteAttr.tests; - }); - - if (!antMode) { - const rootSuite = { - _attr: { - name: this._options.testsuitesTitle, - time: totalSuitesTime.toFixed(4), - tests: totalTests, - failures: stats.failures, - }, - } as XmlSuite; - if (stats.pending) { - rootSuite._attr.skipped = stats.pending; - } - testsuites = [rootSuite].concat(testsuites); - } - - return xml({ testsuites }, { declaration: true, indent: ' ' }); -}; - -/** - * Writes a Sauce JSON test report json document. - */ -MochaJUnitReporter.prototype.writeSauceJsonToDisk = function ( - sauceJson: any, - filePath: string, - fileName: string, -): string { - if (filePath) { - filePath = filePath.replace(/\.xml/, '.json'); - if (filePath.includes('[suite]')) { - filePath = filePath.replace('[suite]', fileName); - } - debug('writing file to', filePath); - mkdirpSync(path.dirname(filePath)); - try { - fs.writeFileSync( - filePath, - JSON.stringify(sauceJson, undefined, 2), - 'utf-8', - ); - } catch (exc) { - console.error(exc); - debug('problem writing results: ' + exc); - } - debug('results written successfully'); - } - return filePath; -}; - -/** - * Writes a JUnit test report XML document. - */ -MochaJUnitReporter.prototype.writeXmlToDisk = function ( - xml: string, - filePath: string, - fileName: string, -): string { - let xmlOutFilePath; - if (filePath) { - if (filePath.includes('[hash]')) { - xmlOutFilePath = filePath.replace('[hash]', md5(xml)); - } else if (filePath.includes('[suite]')) { - xmlOutFilePath = filePath.replace('[suite]', path.basename(fileName)); - } - - debug('writing file to', xmlOutFilePath); - mkdirpSync(path.dirname(xmlOutFilePath)); - - try { - fs.writeFileSync(xmlOutFilePath, xml, 'utf-8'); - } catch (exc) { - debug('problem writing results: ' + exc); - } - debug('results written successfully'); - return xmlOutFilePath; - } - return filePath; -}; - -module.exports = MochaJUnitReporter; -export default MochaJUnitReporter; diff --git a/src/cypress-recorder.ts b/src/cypress-recorder.ts deleted file mode 100644 index fcd97e5a..00000000 --- a/src/cypress-recorder.ts +++ /dev/null @@ -1,32 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import stream from 'stream'; -import childProcess from 'child_process'; - -function cypressRecorder() { - // console.log is saved out of reportsDir since it is cleared on startup. - const fd = fs.openSync(path.join(process.cwd(), 'console.log'), 'w+', 0o644); - const ws = new stream.Writable({ - write(data, encoding, cb) { - fs.write(fd, data, undefined, encoding, cb); - }, - }); - - const [nodeBin] = process.argv; - const child = childProcess.spawn(nodeBin, [ - path.join(__dirname, 'cypress-runner.js'), - ...process.argv.slice(2), - ]); - - child.stdout.pipe(process.stdout); - child.stderr.pipe(process.stderr); - child.stdout.pipe(ws); - child.stderr.pipe(ws); - - child.on('exit', (exitCode) => { - fs.closeSync(fd); - process.exit(exitCode); - }); -} - -exports.cypressRecorder = cypressRecorder; diff --git a/src/cypress-runner.ts b/src/cypress-runner.ts index 0a382cf6..21c93949 100644 --- a/src/cypress-runner.ts +++ b/src/cypress-runner.ts @@ -1,4 +1,3 @@ -import { mergeJUnitFile } from './sauce-reporter'; import path from 'path'; import fs from 'fs'; import { @@ -14,37 +13,20 @@ import cypress from 'cypress'; import util from 'util'; import _ from 'lodash'; import { afterRunTestReport } from '@saucelabs/cypress-plugin'; +import { createJUnitReport } from '@saucelabs/cypress-junit-plugin'; import { RunConfig, Suite } from './types'; async function report( - results: CypressCommandLine.CypressRunResult, - statusCode: number, - browserName: string, + results: + | CypressCommandLine.CypressRunResult + | CypressCommandLine.CypressFailedRunResult, runCfg: RunConfig, - suiteName: string, ) { - // Prepare the assets - const runs = results.runs || []; - const specFiles = runs.map((run) => path.basename(run.spec.name)); - - const failures = results.totalFailed; - let platformName = ''; - for (const c of runCfg.suites) { - if (c.name === suiteName) { - platformName = c.platformName; - break; - } - } - try { - mergeJUnitFile( - specFiles, - runCfg.resultsDir, - suiteName, - browserName, - platformName, - ); + createJUnitReport(results, { + filename: path.join(runCfg.resultsDir, 'junit.xml'), + }); } catch (e) { console.warn('Skipping JUnit file generation:', e); } @@ -59,45 +41,27 @@ async function report( console.error('Failed to serialize test results:', e); } - return failures === 0 && statusCode === 0; -} + if (isFailedRunResult(results)) { + return false; + } -// Configure reporters -function configureReporters(runCfg: RunConfig, opts: any) { - // Enable cypress-multi-reporters plugin - opts.config.reporter = path.join( - __dirname, - '../node_modules/cypress-multi-reporters/lib/MultiReporters.js', - ); - opts.config.reporterOptions = { - configFile: path.join(__dirname, '..', 'sauce-reporter-config.json'), - }; + return results.totalFailed === 0; +} - const customReporter = path.join(__dirname, '../lib/custom-reporter.js'); - const junitReporter = path.join( - __dirname, - '../node_modules/mocha-junit-reporter/index.js', +function isFailedRunResult( + maybe: + | CypressCommandLine.CypressRunResult + | CypressCommandLine.CypressFailedRunResult, +): maybe is CypressCommandLine.CypressFailedRunResult { + return ( + (maybe as CypressCommandLine.CypressFailedRunResult).status === 'failed' ); +} - let defaultSpecRoot = ''; - if (opts.testingType === 'component') { - defaultSpecRoot = 'cypress/component'; - } else { - defaultSpecRoot = 'cypress/e2e'; - } - - // Referencing "mocha-junit-reporter" using relative path will allow to have multiple instance of mocha-junit-reporter. - // That permits to have a configuration specific to us, and in addition to keep customer's one. +// Configure reporters +function configureReporters(runCfg: RunConfig, opts: any) { const reporterConfig = { - reporterEnabled: `spec, ${customReporter}, ${junitReporter}`, - [[_.camelCase(customReporter), 'ReporterOptions'].join('')]: { - mochaFile: `${runCfg.resultsDir}/[suite].xml`, - specRoot: defaultSpecRoot, - }, - [[_.camelCase(junitReporter), 'ReporterOptions'].join('')]: { - mochaFile: `${runCfg.resultsDir}/[suite].xml`, - specRoot: defaultSpecRoot, - }, + reporterEnabled: `spec`, }; // Adding custom reporters @@ -111,11 +75,25 @@ function configureReporters(runCfg: RunConfig, opts: any) { } } + const reporterConfigPath = path.join( + __dirname, + '..', + 'sauce-reporter-config.json', + ); + // Save reporters config - fs.writeFileSync( - path.join(__dirname, '..', 'sauce-reporter-config.json'), - JSON.stringify(reporterConfig), + fs.writeFileSync(reporterConfigPath, JSON.stringify(reporterConfig)); + + // Cypress only supports a single reporter out of the box, so we need to use + // a plugin to support multiple reporters. + opts.config.reporter = path.join( + __dirname, + '../node_modules/cypress-multi-reporters/lib/MultiReporters.js', ); + opts.config.reporterOptions = { + configFile: reporterConfigPath, + }; + return opts; } @@ -163,7 +141,7 @@ function getCypressOpts( } let headed = true; - // suite.config.headless is kepts to keep backward compatibility. + // suite.config.headless is kept to keep backward compatibility. if (suite.headless || suite.config.headless) { headed = false; } @@ -270,40 +248,31 @@ async function cypressRunner( // Execute pre-exec steps if (!(await preExec.run(suite, preExecTimeoutSec))) { - await report( - {} as CypressCommandLine.CypressRunResult, - 0, - cypressOpts.browser, - runCfg, - suiteName, - ); - return; + return false; } // saucectl suite.timeout is in nanoseconds timeoutSec = suite.timeout / 1000000000 || timeoutSec; let timeout: NodeJS.Timeout; - const timeoutPromise = new Promise((resolve) => { - timeout = setTimeout(() => { - console.error(`Test timed out after ${timeoutSec} seconds`); - resolve(false); - }, timeoutSec * 1000); - }); + const timeoutPromise: Promise = + new Promise((resolve) => { + timeout = setTimeout(() => { + console.error(`Test timed out after ${timeoutSec} seconds`); + resolve({ + status: 'failed', + failures: 1, + message: `Test timed out after ${timeoutSec} seconds`, + }); + }, timeoutSec * 1000); + }); const results = await Promise.race([ timeoutPromise, cypress.run(cypressOpts), ]); clearTimeout(timeout); - const statusCode = results ? 0 : 1; - - return await report( - results as CypressCommandLine.CypressRunResult, - statusCode, - cypressOpts.browser, - runCfg, - suiteName, - ); + + return await report(results, runCfg); } // For dev and test purposes, this allows us to run our Cypress Runner from command line @@ -311,17 +280,15 @@ if (require.main === module) { const packageInfo = require(path.join(__dirname, '..', 'package.json')); console.log(`Sauce Cypress Runner ${packageInfo.version}`); console.log(`Running Cypress ${packageInfo.dependencies?.cypress || ''}`); + const { nodeBin, runCfgPath, suiteName } = getArgs(); - // maxTimeout maximum test execution timeout is 1800 seconds (30 mins) - const maxTimeout = 1800; - const maxPreExecTimeout = 300; + const timeoutSec = 1800; // 30 min + const preExecTimeoutSec = 300; // 5 min - cypressRunner(nodeBin, runCfgPath, suiteName, maxTimeout, maxPreExecTimeout) + cypressRunner(nodeBin, runCfgPath, suiteName, timeoutSec, preExecTimeoutSec) .then((passed) => process.exit(passed ? 0 : 1)) .catch((err) => { console.log(err); process.exit(1); }); } - -export { cypressRunner, configureReporters, getSuite, setEnvironmentVariables }; diff --git a/src/sauce-reporter.ts b/src/sauce-reporter.ts deleted file mode 100644 index b23ab4f1..00000000 --- a/src/sauce-reporter.ts +++ /dev/null @@ -1,131 +0,0 @@ -import path from 'path'; -import fs from 'fs'; -import { escapeXML } from 'sauce-testrunner-utils'; -import convert from 'xml-js'; -import { XmlSuiteContainer } from './types'; - -export function mergeJUnitFile( - specFiles: any[], - resultsFolder: string, - testName: string, - browserName: string, - platformName: string, -) { - if (specFiles.length === 0) { - return; - } - - const opts: { - compact: boolean; - spaces: number; - textFn?: (v: string) => string; - } = { compact: true, spaces: 4 }; - const testsuites = []; - for (let i = 0; i < specFiles.length; i++) { - const specJUnitFile = path.join(resultsFolder, `${specFiles[i]}.xml`); - if (!fs.existsSync(specJUnitFile)) { - console.warn( - `JUnit file not found for spec: ${specFiles[i]}. Proceeding without it...`, - ); - continue; - } - const xmlData = fs.readFileSync(specJUnitFile, 'utf8'); - const jsObj = convert.xml2js(xmlData, opts) as XmlSuiteContainer; - if (jsObj.testsuites && jsObj.testsuites.testsuite) { - testsuites.push(...jsObj.testsuites.testsuite); - } - } - if (testsuites.length === 0) { - console.warn('JUnit file generation skipped: no test suites detected.'); - return; - } - - let totalTests = 0; - let totalErr = 0; - let totalFailure = 0; - let totalDisabled = 0; - let totalTime = 0.0; - for (const ts of testsuites) { - if (ts._attributes) { - totalTests += +ts._attributes.tests || 0; - totalFailure += +ts._attributes.failures || 0; - totalTime += +ts._attributes.time || 0.0; - totalErr += +ts._attributes.error || 0; - totalDisabled += +ts._attributes.disabled || 0; - } - } - - const result = { - testsuites: { - testsuite: testsuites.filter( - (item) => item._attributes?.name !== 'Root Suite', - ), - _attributes: { - name: testName, - tests: totalTests, - failures: totalFailure, - time: totalTime, - error: totalErr, - disabled: totalDisabled, - }, - }, - }; - - for (let i = 0; i < result.testsuites.testsuite.length; i++) { - const testcase = result.testsuites.testsuite[i].testcase; - - // _attributes - result.testsuites.testsuite[i]._attributes = - result.testsuites.testsuite[i]._attributes || {}; - result.testsuites.testsuite[i]._attributes.id = i; - - // failure message - if (testcase && testcase.failure) { - result.testsuites.testsuite[i].testcase.failure._attributes = { - message: escapeXML(testcase.failure._attributes.message || ''), - type: testcase.failure._attributes.type || '', - }; - result.testsuites.testsuite[i].testcase.failure._cdata = - testcase.failure._cdata || ''; - } - - // properties - result.testsuites.testsuite[i].properties = { - property: [ - { - _attributes: { - name: 'platformName', - value: getPlatformName(platformName), - }, - }, - { - _attributes: { - name: 'browserName', - value: getBrowsername(browserName), - }, - }, - ], - }; - } - - opts.textFn = escapeXML; - const xmlResult = convert.js2xml(result, opts); - fs.writeFileSync(path.join(resultsFolder, 'junit.xml'), xmlResult); -} - -function getPlatformName(platformName: string) { - if (process.platform.toLowerCase() === 'linux') { - platformName = 'Linux'; - } - - return platformName; -} - -function getBrowsername(browserName: string) { - const browsers = browserName.split(':'); - if (browsers.length > 0) { - browserName = browsers[browsers.length - 1]; - } - - return browserName; -} diff --git a/src/types.ts b/src/types.ts index c8af7678..f0a89b9f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,11 +3,6 @@ import { PathContainer, } from 'sauce-testrunner-utils/lib/types'; -export type MetaData = { - tags: string[]; - build: string; -}; - export type SauceConfig = { region: 'us-west-1' | 'us-east-4' | 'eu-central-1' | 'staging'; metadata: object; @@ -48,68 +43,6 @@ export type RunConfig = { PathContainer & ResultPathContainer; -export type Results = { - runs: any[]; - failures: number; - totalFailed: number; -}; - export type ResultPathContainer = { resultsDir: string; }; - -// XML Types - -export type XmlSuiteAttributes = { - time?: number | string; - failures?: number; - skipped?: number; - tests: number; - errors?: number; - file?: string; - package?: string; - name?: string; - hostname?: string; - id?: string; -}; - -export type XmlSuiteAttrContainer = { - _attr: XmlSuiteAttributes; -}; - -type XmlTestCaseAttributes = { - time: number | string; - failures?: number; - skipped?: number; - classname?: string; - name?: string; -}; - -export type XmlProperties = { - properties: any; -}; - -export type XmlTestCase = { - _attr?: XmlTestCaseAttributes; - 'system-out'?: any; - 'system-err'?: any; - failure?: any; -}; - -export type XmlTestCaseContainer = { - testcase: XmlTestCase[]; -}; - -export type XmlSuite = { - testsuite?: (XmlSuiteAttrContainer | XmlProperties | XmlTestCaseContainer)[]; -} & XmlSuiteAttrContainer; - -export type XmlSuiteContainer = { - testsuites: XmlSuite; -}; - -// Type SauceReporter -export type Metrics = { - name: string; - data: any; -}; diff --git a/tests/unit/cypress.config b/tests/unit/cypress.config deleted file mode 100644 index 0967ef42..00000000 --- a/tests/unit/cypress.config +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/tests/unit/src/custom-reporter.spec.ts b/tests/unit/src/custom-reporter.spec.ts deleted file mode 100644 index cf37dd05..00000000 --- a/tests/unit/src/custom-reporter.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -jest.mock('mkdirp'); -jest.mock('fs'); -jest.mock('sauce-testrunner-utils'); -import { mkdirpSync } from 'mkdirp'; -import fs from 'fs'; -import path from 'path'; -import MochaJUnitReporter from '../../../src/custom-reporter'; - -type Context = { - _runner: any; - flush: jest.Mock; - _options: any; -}; - -describe('Custom Reporter', function () { - describe('.report', function () { - it('calls flush on a spec file with full path', function () { - const { report } = MochaJUnitReporter.prototype; - const ctx = {} as Context; - ctx._runner = { suite: { file: 'spec/folder/path/to/spec' } }; - ctx.flush = jest.fn(); - ctx._options = {}; - ctx._options.specFolder = path.join(process.cwd(), 'spec', 'folder'); - ctx._options.specRoot = path.join(process.cwd(), 'spec', 'folder'); - report.call(ctx, 'a', 'b'); - expect(ctx.flush.mock.calls).toEqual([['a', 'path/to/spec', 'b']]); - }); - it('translates relative paths to absolute paths', function () { - const { report } = MochaJUnitReporter.prototype; - const ctx = {} as Context; - ctx._runner = { suite: { file: 'spec/folder/path/to/spec' } }; - ctx.flush = jest.fn(); - ctx._options = {}; - ctx._options.specFolder = path.join('spec', 'folder'); - ctx._options.specRoot = path.join('spec', 'folder'); - report.call(ctx, 'a', 'b'); - expect(ctx.flush.mock.calls).toEqual([['a', 'path/to/spec', 'b']]); - }); - }); - describe('.writeXmlToDisk', function () { - it('maintains relative paths correctly (addresses DEVX-273)', function () { - const { writeXmlToDisk } = MochaJUnitReporter.prototype; - const xml = ` - - One - Two - Three - - `.trim(); - const filepath = '/path/to/[suite].xml'; - const filename = 'subdir-a/subdir-b/test.spec.js'; - jest.mocked(mkdirpSync).mockImplementation(); - jest.mocked(fs.writeFileSync).mockImplementation(); - writeXmlToDisk(xml, filepath, filename); - expect(jest.mocked(fs.writeFileSync).mock.calls).toEqual([ - ['/path/to/test.spec.js.xml', xml, 'utf-8'], - ]); - }); - }); -});