Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 115 additions & 3 deletions lib/plugin/stepByStepReport.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ const templates = {}
* npx codeceptjs run --plugins stepByStepReport
* ```
*
* Run tests with workers:
*
* ```
* npx codeceptjs run-workers 2 --plugins stepByStepReport
* ```
*
* Run tests with multiple configurations:
*
* ```
* npx codeceptjs run-multiple --all --plugins stepByStepReport
* ```
*
* #### Configuration
*
* ```js
Expand All @@ -60,6 +72,20 @@ const templates = {}
* * `screenshotsForAllureReport`: If Allure plugin is enabled this plugin attaches each saved screenshot to allure report. Default: false.
* * `disableScreenshotOnFail : Disables the capturing of screeshots after the failed step. Default: true.
*
* #### Worker and Multiple Run Support
*
* When using `run-workers`, `run-multiple`, or combinations thereof, the plugin automatically
* detects all worker and run processes and creates a consolidated step-by-step report that
* includes screenshots from all processes while keeping them in their original directories.
*
* Screenshots remain in their respective process directories for traceability:
* - **run-workers**: Screenshots saved in `/output/worker1/`, `/output/worker2/`, etc.
* - **run-multiple**: Screenshots saved in `/output/config_name_hash/`, etc.
* - **Mixed scenarios**: Screenshots saved in `/output/config_name_hash/worker1/`, etc.
*
* The final consolidated report links to all screenshots while preserving their original locations
* and indicating which process or worker they came from.
*
* @param {*} config
*/

Expand Down Expand Up @@ -87,8 +113,12 @@ module.exports = function (config) {

const recordedTests = {}
const pad = '0000'

const reportDir = config.output ? path.resolve(global.codecept_dir, config.output) : defaultConfig.output

// Ensure the report directory exists
mkdirp.sync(reportDir)

event.dispatcher.on(event.suite.before, suite => {
stepNum = -1
})
Expand Down Expand Up @@ -137,11 +167,70 @@ module.exports = function (config) {

event.dispatcher.on(event.workers.result, async () => {
await recorder.add(() => {
const recordedTests = getRecordFoldersWithDetails(reportDir)
// For workers and run-multiple scenarios, we need to search across multiple directories
// to find all screenshot folders from different processes
const recordedTests = getRecordFoldersFromAllDirectories()
generateRecordsHtml(recordedTests)
})
})

function getRecordFoldersFromAllDirectories() {
let results = {}

// Determine the base output directory to search from
const baseOutputDir = config.output ? path.resolve(global.codecept_dir, config.output) : defaultConfig.output

// Function to recursively search for record folders in a directory
function searchForRecordFolders(searchDir, basePath = '') {
try {
if (!fs.existsSync(searchDir)) return

const items = fs.readdirSync(searchDir, { withFileTypes: true })

items.forEach(item => {
if (item.isDirectory()) {
const itemPath = path.join(searchDir, item.name)
const relativePath = basePath ? path.join(basePath, item.name) : item.name

// If this is a record folder, process it
if (item.name.startsWith('record_')) {
const indexPath = path.join(itemPath, 'index.html')

let name = ''
if (fs.existsSync(indexPath)) {
try {
const htmlContent = fs.readFileSync(indexPath, 'utf-8')
const $ = cheerio.load(htmlContent)
name = $('.navbar-brand').text().trim()
} catch (err) {
console.error(`Error reading index.html in ${itemPath}:`, err.message)
}
}

// Include the relative path to show which process/worker this came from
const displayName = basePath ? `${name} (${basePath})` : name
results[displayName || 'Unknown'] = path.join(relativePath, 'index.html')
} else {
// Continue searching in subdirectories (worker folders, run-multiple folders)
searchForRecordFolders(itemPath, relativePath)
}
}
})
} catch (err) {
console.error(`Error searching directory ${searchDir}:`, err.message)
}
}

// Start the search from the base output directory
searchForRecordFolders(baseOutputDir)

// Also check the current reportDir for backwards compatibility
const currentDirResults = getRecordFoldersWithDetails(reportDir)
Object.assign(results, currentDirResults)

return results
}

function getRecordFoldersWithDetails(dirPath) {
let results = {}

Expand Down Expand Up @@ -186,9 +275,32 @@ module.exports = function (config) {
records: links,
})

fs.writeFileSync(path.join(reportDir, 'records.html'), indexHTML)
// Determine where to write the main records.html file
// For worker/run-multiple scenarios, we want to write to the base output directory
let recordsHtmlDir = reportDir

if (global.codecept_dir && (process.env.RUNS_WITH_WORKERS === 'true' || process.argv.some(arg => arg === '--child'))) {
// Extract base output directory by removing worker/run-specific segments
const baseOutputDir = config.output ? path.resolve(global.codecept_dir, config.output) : defaultConfig.output
let actualBaseDir = baseOutputDir

// For workers: strip worker directory segment
if (process.env.RUNS_WITH_WORKERS === 'true') {
actualBaseDir = actualBaseDir.replace(/[/\\][^/\\]+$/, '')
}

// For run-multiple: strip run directory segment
if (process.argv.some(arg => arg === '--child')) {
actualBaseDir = actualBaseDir.replace(/[/\\][^/\\]+$/, '')
}

recordsHtmlDir = actualBaseDir
mkdirp.sync(recordsHtmlDir)
}

fs.writeFileSync(path.join(recordsHtmlDir, 'records.html'), indexHTML)

output.print(`${figures.circleFilled} Step-by-step preview: ${colors.white.bold(`file://${reportDir}/records.html`)}`)
output.print(`${figures.circleFilled} Step-by-step preview: ${colors.white.bold(`file://${recordsHtmlDir}/records.html`)}`)
}

async function persistStep(step) {
Expand Down
229 changes: 229 additions & 0 deletions test/acceptance/stepByStepReport_regression_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
const path = require('path')
const fs = require('fs')
const { exec } = require('child_process')
const { expect } = require('expect')

const runner = path.join(__dirname, '../../bin/codecept.js')
const codecept_dir = path.join(__dirname, '../data/sandbox')

describe('stepByStepReport regression tests', function () {
this.timeout(120000) // Increased timeout for acceptance tests

const outputDir = path.join(codecept_dir, 'output')

beforeEach(() => {
// Clean up output directory before each test
if (fs.existsSync(outputDir)) {
fs.rmSync(outputDir, { recursive: true, force: true })
}
})

afterEach(() => {
// Clean up output directory after each test
if (fs.existsSync(outputDir)) {
fs.rmSync(outputDir, { recursive: true, force: true })
}
})

function createConfig(name, extraConfig = {}) {
const config = `
// Override the standard acting helpers to include FakeDriver for testing
const Container = require('../../lib/container')
const originalHelpers = Container.STANDARD_ACTING_HELPERS
Object.defineProperty(Container, 'STANDARD_ACTING_HELPERS', {
get: () => [...originalHelpers, 'FakeDriver']
})

exports.config = {
tests: './stepbystep_test.js',
timeout: 30000,
output: './output',
helpers: {
FakeDriver: {
require: '../fake_driver',
browser: 'dummy',
windowSize: '1024x768'
},
},
plugins: {
stepByStepReport: {
enabled: true,
deleteSuccessful: false,
...${JSON.stringify(extraConfig)}
},
},
include: {},
bootstrap: false,
mocha: {},
name: '${name}',
}
`
const configPath = path.join(codecept_dir, `codecept.${name}.js`)
fs.writeFileSync(configPath, config)
return configPath
}

function runCommand(command) {
return new Promise((resolve, reject) => {
exec(command, { cwd: codecept_dir }, (err, stdout, stderr) => {
resolve({ err, stdout, stderr })
})
})
}

it('should handle run-workers without consolidating screenshots', async function () {
const configPath = createConfig('workers-test')

const command = `${runner} run-workers 2 --config ${configPath} --grep "@stepbystep"`
const result = await runCommand(command)

console.log('STDOUT:', result.stdout)
console.log('STDERR:', result.stderr)

// Key regression test: ensure no stepByStepReport consolidation directory
const consolidatedDir = path.join(outputDir, 'stepByStepReport')
expect(fs.existsSync(consolidatedDir)).toBe(false)

console.log('✓ No stepByStepReport consolidation directory created for run-workers')

// Verify basic functionality without requiring screenshots
expect(result.stdout).toContain('CodeceptJS')

// Clean up
fs.unlinkSync(configPath)
})

it('should handle run-multiple without consolidating screenshots', async function () {
const multipleConfig = `
// Override the standard acting helpers to include FakeDriver for testing
const Container = require('../../lib/container')
const originalHelpers = Container.STANDARD_ACTING_HELPERS
Object.defineProperty(Container, 'STANDARD_ACTING_HELPERS', {
get: () => [...originalHelpers, 'FakeDriver']
})

exports.config = {
tests: './stepbystep_test.js',
timeout: 30000,
output: './output',
helpers: {
FakeDriver: {
require: '../fake_driver',
browser: 'dummy',
windowSize: '1024x768'
},
},
plugins: {
stepByStepReport: {
enabled: true,
deleteSuccessful: false,
},
},
include: {},
bootstrap: false,
mocha: {},
name: 'multiple-test',
multiple: {
basic: {
browsers: ['chrome']
}
}
}
`

const configPath = path.join(codecept_dir, 'codecept.multiple.js')
fs.writeFileSync(configPath, multipleConfig)

const command = `${runner} run-multiple basic --config ${configPath} --grep "@stepbystep"`
const result = await runCommand(command)

console.log('STDOUT:', result.stdout)
console.log('STDERR:', result.stderr)

// Key regression test: ensure no stepByStepReport consolidation directory
const consolidatedDir = path.join(outputDir, 'stepByStepReport')
expect(fs.existsSync(consolidatedDir)).toBe(false)

console.log('✓ No stepByStepReport consolidation directory created for run-multiple')

// Verify that the command runs successfully with run-multiple
expect(result.stdout).toContain('CodeceptJS')

// Clean up
fs.unlinkSync(configPath)
})

it('should handle regular run command with backward compatibility', async function () {
const configPath = createConfig('regular')

const command = `${runner} run --config ${configPath} --grep "@stepbystep"`
const result = await runCommand(command)

console.log('STDOUT:', result.stdout)
console.log('STDERR:', result.stderr)

// Key regression test: ensure no stepByStepReport consolidation directory
const consolidatedDir = path.join(outputDir, 'stepByStepReport')
expect(fs.existsSync(consolidatedDir)).toBe(false)

console.log('✓ No stepByStepReport consolidation directory created for regular run')

// Verify backward compatibility - regular run should work
expect(result.stdout).toContain('CodeceptJS')

// Clean up
fs.unlinkSync(configPath)
})

it('should handle custom output directories', async function () {
const configPath = createConfig('custom-output', { output: './output/custom' })

const command = `${runner} run-workers 2 --config ${configPath} --grep "@stepbystep"`
const result = await runCommand(command)

console.log('STDOUT:', result.stdout)
console.log('STDERR:', result.stderr)

// Check that no consolidation happens in any output directory
const mainConsolidatedDir = path.join(outputDir, 'stepByStepReport')
const customConsolidatedDir = path.join(outputDir, 'custom', 'stepByStepReport')

expect(fs.existsSync(mainConsolidatedDir)).toBe(false)
expect(fs.existsSync(customConsolidatedDir)).toBe(false)

console.log('✓ No stepByStepReport consolidation directory created with custom output')

// Clean up
fs.unlinkSync(configPath)
})

it('should not crash with stepByStepReport plugin enabled', async function () {
// This test ensures the plugin initialization and basic operations work
// without causing crashes across different execution modes
const configPath = createConfig('no-crash-test')

const commands = [`${runner} run --config ${configPath} --grep "@stepbystep"`, `${runner} run-workers 2 --config ${configPath} --grep "@stepbystep"`]

for (const command of commands) {
const result = await runCommand(command)

console.log(`Command: ${command}`)
console.log('STDOUT:', result.stdout)
console.log('STDERR:', result.stderr)

// Ensure the plugin doesn't cause crashes
expect(result.stdout).toContain('CodeceptJS')
expect(result.stderr).not.toContain('Cannot read properties of undefined')
expect(result.stderr).not.toContain('TypeError')

// Key regression test
const consolidatedDir = path.join(outputDir, 'stepByStepReport')
expect(fs.existsSync(consolidatedDir)).toBe(false)
}

console.log('✓ Plugin works without crashes across execution modes')

// Clean up
fs.unlinkSync(configPath)
})
})
Loading
Loading