diff --git a/README.md b/README.md index d7dd797..bd5a0ea 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,7 @@ Add extra information to the report manually by using `cy.addTestContext()` as s 3. [With `mochawesome-report-generator` flags](examples/mochawesome-flags) 4. [Change default screenshots folder in `cypress.json`](examples/screenshots-folder) 5. [Using `cypress-mochawesome-reporter` with typescript](examples/simple-typescript) +6. [Using `cypress-mochawesome-reporter` with `cypress-parallel`](examples/cypress-parallel) Run `npm i` in root directory then: diff --git a/cli.js b/cli.js new file mode 100755 index 0000000..df8cc0d --- /dev/null +++ b/cli.js @@ -0,0 +1,43 @@ +#!/usr/bin/env node + +const { Command } = require('commander'); +const fse = require('fs-extra'); +const { setSimpleConfig } = require('./lib/config'); +const { log, initDebugLog, debugLog } = require('./lib/logger'); +const generateReport = require('./lib/generateReport'); +const consts = require('./lib/consts'); +const packageJson = require('./package.json'); + +(async () => { + const program = new Command(); + program + .name('generate-mochawesome-report') + .description('CLI merge and generate Cypress Mochawesome report') + .version(packageJson.version) + .option('-c, --config ', 'should be the same as "configOutput" reporter option', consts.defaultConfigOutput) + .option('-o, --output ', 'report output folder') + .option('--debug', 'print debug logs', false); + + program.parse(); + const options = program.opts(); + + initDebugLog(options.debug === true); + + debugLog(`cwd: ${process.cwd()}`); + + log(`read config from ${options.config}`); + + const config = await fse.readJson(options.config); + debugLog(`config: ${JSON.stringify(config)}`); + + if (options.output) { + debugLog(`override output with: ${options.output}`); + config.outputDir = options.output; + config.reporterOptions.reportDir = options.output; + } + + setSimpleConfig(config); + + log('generate report'); + await generateReport(); +})(); diff --git a/cypress/e2e/reportOutput.cy.js b/cypress/e2e/reportOutput.cy.js index ed8a4a3..a76d70a 100644 --- a/cypress/e2e/reportOutput.cy.js +++ b/cypress/e2e/reportOutput.cy.js @@ -6,6 +6,7 @@ describe('Report output', () => { 'mochawesome-flags', 'screenshots-folder', 'simple-typescript', + 'cypress-parallel', ].forEach((folder) => { describe(`${folder} folder`, () => { beforeEach(() => { @@ -57,7 +58,7 @@ describe('Report output', () => { }); describe('Video output', () => { - ['simple', 'screenshots-folder', 'simple-typescript'].forEach((folder) => { + ['simple', 'screenshots-folder', 'simple-typescript', 'cypress-parallel'].forEach((folder) => { describe(`Validate video exists in ${folder} folder`, () => { beforeEach(() => { cy.visit(`examples/${folder}/cypress/reports/html/index.html`); diff --git a/examples/cypress-parallel/.gitignore b/examples/cypress-parallel/.gitignore new file mode 100644 index 0000000..38ea3ce --- /dev/null +++ b/examples/cypress-parallel/.gitignore @@ -0,0 +1,7 @@ +node_modules +cypress/reports +cypress/screenshots +cypress/videos +runner-results +multi-reporter-config.json +.tmp diff --git a/examples/cypress-parallel/README.md b/examples/cypress-parallel/README.md new file mode 100644 index 0000000..be161a8 --- /dev/null +++ b/examples/cypress-parallel/README.md @@ -0,0 +1,50 @@ +# Setup with `cypress-parallel` + +1. Follow the steps in the [main README](../../README.md) to setup the reporter. + +1. Change `cypress.config.js` to only use `beforeRunHook`, otherwise the reporter will try to create multiple HTML reporters without all the tests. + + ```js + const { defineConfig } = require('cypress'); + const { beforeRunHook } = require('cypress-mochawesome-reporter/lib'); + + module.exports = defineConfig({ + video: true, + retries: 0, + e2e: { + setupNodeEvents(on, config) { + on('before:run', async (details) => { + await beforeRunHook(details); + }); + }, + }, + }); + ``` + +1. How to run: + + 1. Because cypress parallel runs in threads you will need to manually clear the report output folder before run. + + 1. Set `cypress-parallel` reporter to `cypress-mochawesome-reporter` + + ```sh + cypress-parallel -s cy:run -t 2 -d 'cypress/e2e/**/*.cy.js' -r 'cypress-mochawesome-reporter' -o 'cypressParallel=true'" + ``` + + 1. Create report after test run: + + ```sh + npx generate-mochawesome-report + ``` + + Example scripts section in `package.json`: + + ```json + "scripts": { + "cy:run": "cypress run", + "cy:run:parallel": "cypress-parallel -s cy:run -t 2 -d 'cypress/e2e/**/*.cy.js' -r 'cypress-mochawesome-reporter' -o 'cypressParallel=true'", + "clean": "rimraf cypress/reports", + "generate-report": "generate-mochawesome-report", + "test": "npm run clean && npm run cy:run:parallel || true && npm run generate-report" + }, + ``` diff --git a/examples/cypress-parallel/cypress.config.js b/examples/cypress-parallel/cypress.config.js new file mode 100644 index 0000000..61e3a35 --- /dev/null +++ b/examples/cypress-parallel/cypress.config.js @@ -0,0 +1,14 @@ +const { defineConfig } = require('cypress'); +const { beforeRunHook } = require('cypress-mochawesome-reporter/lib'); + +module.exports = defineConfig({ + video: true, + retries: 0, + e2e: { + setupNodeEvents(on, config) { + on('before:run', async (details) => { + await beforeRunHook(details); + }); + }, + }, +}); diff --git a/examples/cypress-parallel/cypress/e2e/sub-dir/test1.cy.js b/examples/cypress-parallel/cypress/e2e/sub-dir/test1.cy.js new file mode 100644 index 0000000..21b978f --- /dev/null +++ b/examples/cypress-parallel/cypress/e2e/sub-dir/test1.cy.js @@ -0,0 +1,27 @@ +it('fail test #tag1', () => { + cy.visit('site/index.html'); + + cy.screenshot('custom-name'); + + cy.get('#todo-list li').should('have.length', 10); +}); + +describe('Test 1 #tag2', () => { + it('default todos exists', () => { + cy.visit('site/index.html'); + + cy.get('#todo-list li').should('have.length', 4); + + cy.screenshot(); + }); + + describe('hierarchy', () => { + it('fail test hierarchy #tag3', () => { + cy.visit('site/index.html'); + + cy.screenshot('custom-name2'); + + cy.get('#todo-list li').should('have.length', 10); + }); + }); +}); diff --git a/examples/cypress-parallel/cypress/e2e/test2.cy.js b/examples/cypress-parallel/cypress/e2e/test2.cy.js new file mode 100644 index 0000000..22f2339 --- /dev/null +++ b/examples/cypress-parallel/cypress/e2e/test2.cy.js @@ -0,0 +1,19 @@ +describe('Test 2', () => { + it('todo exists', () => { + cy.visit('site/index.html'); + + cy.get('#todo-list').should('be.visible'); + }); + + it('add context to mochawesome report', () => { + cy.visit('site/index.html'); + + cy.get('#todo-list > li').then(($liElements) => { + cy.addTestContext(`There were ${$liElements.length.toString()} items found in the todo-list`); + }); + }); + + it.skip('skipped test', () => { + cy.visit('site/index.html'); + }); +}); diff --git a/examples/cypress-parallel/cypress/e2e/test3.cy.js b/examples/cypress-parallel/cypress/e2e/test3.cy.js new file mode 100644 index 0000000..c3618c9 --- /dev/null +++ b/examples/cypress-parallel/cypress/e2e/test3.cy.js @@ -0,0 +1,11 @@ +describe('Test 3 - fail before hook', () => { + before(() => { + cy.visit('site/index.html'); + + cy.get('#todo-list li').should('have.length', 10); + }); + + it('before hook fail', () => { + cy.visit('site/index.html'); + }); +}); diff --git a/examples/cypress-parallel/cypress/e2e/test4.cy.js b/examples/cypress-parallel/cypress/e2e/test4.cy.js new file mode 100644 index 0000000..ff12bf7 --- /dev/null +++ b/examples/cypress-parallel/cypress/e2e/test4.cy.js @@ -0,0 +1,15 @@ +describe('Test 4 - fail beforeEach hook', () => { + beforeEach(() => { + cy.visit('site/index.html'); + + cy.get('#todo-list li').should('have.length', 10); + }); + + it('beforeEach hook fail 1', () => { + cy.visit('site/index.html'); + }); + + it('beforeEach hook fail 2', () => { + cy.visit('site/index.html'); + }); +}); diff --git a/examples/cypress-parallel/cypress/support/commands.js b/examples/cypress-parallel/cypress/support/commands.js new file mode 100644 index 0000000..e69de29 diff --git a/examples/cypress-parallel/cypress/support/e2e.js b/examples/cypress-parallel/cypress/support/e2e.js new file mode 100644 index 0000000..444b4e0 --- /dev/null +++ b/examples/cypress-parallel/cypress/support/e2e.js @@ -0,0 +1,22 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; + +import 'cypress-mochawesome-reporter/register'; + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/examples/cypress-parallel/package.json b/examples/cypress-parallel/package.json new file mode 100644 index 0000000..ee91644 --- /dev/null +++ b/examples/cypress-parallel/package.json @@ -0,0 +1,18 @@ +{ + "name": "@example/simple", + "version": "1.0.0", + "scripts": { + "cy:run": "cypress run", + "cy:run:parallel": "cypress-parallel -s cy:run -t 2 -d 'cypress/e2e/**/*.cy.js' -r 'cypress-mochawesome-reporter' -o 'cypressParallel=true'", + "clean": "rimraf cypress/reports", + "generate-report": "generate-mochawesome-report", + "test": "npm run clean && npm run cy:run:parallel || true && npm run generate-report" + }, + "license": "MIT", + "devDependencies": { + "cypress": "^12.3.0", + "cypress-mochawesome-reporter": "../../", + "cypress-parallel": "^0.13.0", + "rimraf": "^5.0.1" + } +} diff --git a/examples/cypress-parallel/site/index.html b/examples/cypress-parallel/site/index.html new file mode 100644 index 0000000..a0438fc --- /dev/null +++ b/examples/cypress-parallel/site/index.html @@ -0,0 +1,11 @@ + + +

TODO list

+ + + \ No newline at end of file diff --git a/lib/config.js b/lib/config.js index 7bcbb13..6511376 100644 --- a/lib/config.js +++ b/lib/config.js @@ -30,7 +30,11 @@ function getConfig() { return _config; } -module.exports = { setConfig, getConfig }; +function setSimpleConfig(config) { + _config = config; +} + +module.exports = { setConfig, getConfig, setSimpleConfig }; async function extractConfigFileOptions(configFile) { const configFilePath = path.resolve(configFile); @@ -75,11 +79,42 @@ async function getSimpleCypressConfig(config) { const jsonDir = path.join(reporterOptions.reportDir, '/.jsons'); + if (reporterOptions.cypressParallel === 'true') { + // cypress-parallel is passing booleans as strings, covert them to booleans + for (const key in reporterOptions) { + const value = reporterOptions[key]; + + if (value === 'true') { + reporterOptions[key] = true; + } else if (value === 'false') { + reporterOptions[key] = false; + } + } + + // because we run in parallel, we dont want to overwrite the JSONs from other threads + reporterOptions.overwrite = false; + + reporterOptions.configOutput = reporterOptions.configOutput || true; + reporterOptions.removeJsonsFolderAfterMerge = false; + } + + let { configOutput, removeJsonsFolderAfterMerge, ...restReporterOptions } = reporterOptions; + + if (configOutput !== undefined && configOutput !== false) { + configOutput = typeof configOutput === 'string' ? configOutput : consts.defaultConfigOutput; + } + + if (removeJsonsFolderAfterMerge === undefined) { + removeJsonsFolderAfterMerge = true; + } + return { jsonDir, - reporterOptions, + reporterOptions: restReporterOptions, screenshotsDir: config.screenshotsFolder, videosFolder: config.videosFolder, outputDir: reporterOptions.reportDir, + configOutput, + removeJsonsFolderAfterMerge, }; } diff --git a/lib/consts.js b/lib/consts.js index a50cd59..3e57a21 100644 --- a/lib/consts.js +++ b/lib/consts.js @@ -2,4 +2,5 @@ const path = require('path'); module.exports = { defaultHtmlOutputFolder: 'cypress/reports/html', -} + defaultConfigOutput: 'cypress/.tmp/cypressMochawesomeReporterConfig.json', +}; diff --git a/lib/generateReport.js b/lib/generateReport.js index 8e53433..c0b27f6 100644 --- a/lib/generateReport.js +++ b/lib/generateReport.js @@ -36,7 +36,6 @@ async function copyMediaDir(inputDir, outputDir) { const isExists = fse.existsSync(inputDir); if (isExists) { - if (inputDir !== outputDir) { log(`Copy media folder from "${inputDir}" to "${outputDir}"`); @@ -50,7 +49,8 @@ async function copyMediaDir(inputDir, outputDir) { async function generateReport() { log('Start generate report process'); - const { outputDir, reporterOptions, screenshotsDir, videosFolder, jsonDir } = getConfig(); + const { outputDir, reporterOptions, screenshotsDir, videosFolder, jsonDir, removeJsonsFolderAfterMerge } = + getConfig(); const actions = [mergeAndCreate(jsonDir, screenshotsDir, reporterOptions)]; @@ -66,7 +66,9 @@ async function generateReport() { log('HTML report successfully created!'); log(htmlPath); - await fse.remove(jsonDir); + if (removeJsonsFolderAfterMerge) { + await fse.remove(jsonDir); + } } module.exports = generateReport; diff --git a/lib/index.js b/lib/index.js index 6313156..7f31883 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,4 +1,5 @@ const fse = require('fs-extra'); +const path = require('path'); const { setConfig } = require('./config'); const { log, debugLog } = require('./logger'); const generateReport = require('./generateReport'); @@ -11,7 +12,17 @@ async function beforeRunHook({ config }) { return; } - const { outputDir, jsonDir, reporterOptions } = await setConfig(config); + const simpleConfig = await setConfig(config); + const { outputDir, jsonDir, reporterOptions, configOutput } = simpleConfig; + + if (configOutput) { + debugLog(`Write simple config to ${configOutput}`); + // create folder if not exist + await fse.mkdirp(path.dirname(configOutput)); + + // write simple config to file + await fse.writeJSON(configOutput, simpleConfig, { spaces: 2 }); + } // Only if overwrite is set to false delete only .jsons folder and keep the html files // By default overwrite is set to undefined (which means true by mochawesome-report-generator) diff --git a/package-lock.json b/package-lock.json index 5d634b9..742b246 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,15 @@ "version": "3.5.1", "license": "MIT", "dependencies": { + "commander": "^10.0.1", "fs-extra": "^10.0.1", "mochawesome": "^7.1.3", "mochawesome-merge": "^4.2.1", "mochawesome-report-generator": "^6.2.0" }, + "bin": { + "generate-mochawesome-report": "cli.js" + }, "devDependencies": { "cypress": "^12.3.0", "lerna": "^5.6.1" @@ -3262,12 +3266,11 @@ } }, "node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true, + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "engines": { - "node": ">= 6" + "node": ">=14" } }, "node_modules/common-ancestor-path": { @@ -3582,6 +3585,15 @@ "integrity": "sha512-kdRWLBIJwdsYJWYJFtAFFYxybguqeF91qpZaggjG5Nf8QKdizFG2hjqvaTXbxFIcYbSaD74KpAXv6BSm17DHEQ==", "dev": true }, + "node_modules/cypress/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/cypress/node_modules/debug": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", @@ -12162,10 +12174,9 @@ } }, "commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==" }, "common-ancestor-path": { "version": "1.0.1", @@ -12428,6 +12439,12 @@ "integrity": "sha512-kdRWLBIJwdsYJWYJFtAFFYxybguqeF91qpZaggjG5Nf8QKdizFG2hjqvaTXbxFIcYbSaD74KpAXv6BSm17DHEQ==", "dev": true }, + "commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true + }, "debug": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", diff --git a/package.json b/package.json index 136f21c..10beb1b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cypress-mochawesome-reporter", - "version": "3.5.1", + "version": "3.6.0-rc.0", "description": "Zero config Mochawesome reporter for Cypress with screenshots", "engines": { "node": ">=14" @@ -14,6 +14,9 @@ }, "main": "lib/reporter.js", "types": "index.d.ts", + "bin": { + "generate-mochawesome-report": "cli.js" + }, "scripts": { "lerna": "lerna", "install-examples": "lerna exec --scope @example/* \"npm i\"", @@ -30,6 +33,7 @@ "video" ], "dependencies": { + "commander": "^10.0.1", "fs-extra": "^10.0.1", "mochawesome": "^7.1.3", "mochawesome-merge": "^4.2.1",