Skip to content
Merged
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
1 change: 1 addition & 0 deletions lib/codecept.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ class Codecept {
runHook(require('./listener/helpers'))
runHook(require('./listener/globalTimeout'))
runHook(require('./listener/globalRetry'))
runHook(require('./listener/retryEnhancer'))
runHook(require('./listener/exit'))
runHook(require('./listener/emptyRun'))

Expand Down
26 changes: 24 additions & 2 deletions lib/helper/Mochawesome.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,20 @@ class Mochawesome extends Helper {
}

_test(test) {
currentTest = { test }
// If this is a retried test, we want to add context to the retried test
// but also potentially preserve context from the original test
const originalTest = test.retriedTest && test.retriedTest()
if (originalTest) {
// This is a retried test - use the retried test for context
currentTest = { test }

// Optionally copy context from original test if it exists
// Note: mochawesome context is stored in test.ctx, but we need to be careful
// not to break the mocha context structure
} else {
// Normal test (not a retry)
currentTest = { test }
}
}

_failed(test) {
Expand All @@ -64,7 +77,16 @@ class Mochawesome extends Helper {

addMochawesomeContext(context) {
if (currentTest === '') currentTest = { test: currentSuite.ctx.test }
return this._addContext(currentTest, context)

// For retried tests, make sure we're adding context to the current (retried) test
// not the original test
let targetTest = currentTest
if (currentTest.test && currentTest.test.retriedTest && currentTest.test.retriedTest()) {
// This test has been retried, make sure we're using the current test for context
targetTest = { test: currentTest.test }
}

return this._addContext(targetTest, context)
}
}

Expand Down
85 changes: 85 additions & 0 deletions lib/listener/retryEnhancer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
const event = require('../event')
const { enhanceMochaTest } = require('../mocha/test')

/**
* Enhance retried tests by copying CodeceptJS-specific properties from the original test
* This fixes the issue where Mocha's shallow clone during retries loses CodeceptJS properties
*/
module.exports = function () {
event.dispatcher.on(event.test.before, test => {
// Check if this test is a retry (has a reference to the original test)
const originalTest = test.retriedTest && test.retriedTest()

if (originalTest) {
// This is a retried test - copy CodeceptJS-specific properties from the original
copyCodeceptJSProperties(originalTest, test)

// Ensure the test is enhanced with CodeceptJS functionality
enhanceMochaTest(test)
}
})
}

/**
* Copy CodeceptJS-specific properties from the original test to the retried test
* @param {CodeceptJS.Test} originalTest - The original test object
* @param {CodeceptJS.Test} retriedTest - The retried test object
*/
function copyCodeceptJSProperties(originalTest, retriedTest) {
// Copy CodeceptJS-specific properties
if (originalTest.opts !== undefined) {
retriedTest.opts = originalTest.opts ? { ...originalTest.opts } : {}
}

if (originalTest.tags !== undefined) {
retriedTest.tags = originalTest.tags ? [...originalTest.tags] : []
}

if (originalTest.notes !== undefined) {
retriedTest.notes = originalTest.notes ? [...originalTest.notes] : []
}

if (originalTest.meta !== undefined) {
retriedTest.meta = originalTest.meta ? { ...originalTest.meta } : {}
}

if (originalTest.artifacts !== undefined) {
retriedTest.artifacts = originalTest.artifacts ? [...originalTest.artifacts] : []
}

if (originalTest.steps !== undefined) {
retriedTest.steps = originalTest.steps ? [...originalTest.steps] : []
}

if (originalTest.config !== undefined) {
retriedTest.config = originalTest.config ? { ...originalTest.config } : {}
}

if (originalTest.inject !== undefined) {
retriedTest.inject = originalTest.inject ? { ...originalTest.inject } : {}
}

// Copy methods that might be missing
if (originalTest.addNote && !retriedTest.addNote) {
retriedTest.addNote = function (type, note) {
this.notes = this.notes || []
this.notes.push({ type, text: note })
}
}

if (originalTest.applyOptions && !retriedTest.applyOptions) {
retriedTest.applyOptions = originalTest.applyOptions.bind(retriedTest)
}

if (originalTest.simplify && !retriedTest.simplify) {
retriedTest.simplify = originalTest.simplify.bind(retriedTest)
}

// Preserve the uid if it exists
if (originalTest.uid !== undefined) {
retriedTest.uid = originalTest.uid
}

// Mark as enhanced
retriedTest.codeceptjs = true
}
98 changes: 98 additions & 0 deletions test/unit/mocha/mochawesome_retry_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
const { expect } = require('chai')
const { createTest } = require('../../../lib/mocha/test')
const { createSuite } = require('../../../lib/mocha/suite')
const MochaSuite = require('mocha/lib/suite')
const Test = require('mocha/lib/test')
const Mochawesome = require('../../../lib/helper/Mochawesome')
const retryEnhancer = require('../../../lib/listener/retryEnhancer')
const event = require('../../../lib/event')

describe('MochawesomeHelper with retries', function () {
let helper

beforeEach(function () {
helper = new Mochawesome({})
// Setup the retryEnhancer
retryEnhancer()
})

it('should add context to the correct test object when test is retried', function () {
// Create a CodeceptJS enhanced test
const originalTest = createTest('Test with mochawesome context', () => {})

// Create a mock suite and set up context
const rootSuite = new MochaSuite('', null, true)
const suite = createSuite(rootSuite, 'Test Suite')
originalTest.addToSuite(suite)

// Set some CodeceptJS-specific properties
originalTest.opts = { timeout: 5000 }
originalTest.meta = { feature: 'reporting' }

// Simulate what happens during mocha retries - using mocha's native clone method
const retriedTest = Test.prototype.clone.call(originalTest)

// Trigger the retryEnhancer to copy properties
event.emit(event.test.before, retriedTest)

// Verify that properties were copied
expect(retriedTest.opts).to.deep.equal({ timeout: 5000 })
expect(retriedTest.meta).to.deep.equal({ feature: 'reporting' })

// Now simulate the test lifecycle hooks
helper._beforeSuite(suite)
helper._test(retriedTest) // This should set currentTest to the retried test

// Add some context using the helper
const contextData = { screenshot: 'test.png', url: 'http://example.com' }

// Mock the _addContext method to capture what test object is passed
let contextAddedToTest = null
helper._addContext = function (testWrapper, context) {
contextAddedToTest = testWrapper.test
return Promise.resolve()
}

// Add context
helper.addMochawesomeContext(contextData)

// The context should be added to the retried test, not the original
expect(contextAddedToTest).to.equal(retriedTest)
expect(contextAddedToTest).to.not.equal(originalTest)

// Verify the retried test has the enhanced properties
expect(contextAddedToTest.opts).to.deep.equal({ timeout: 5000 })
expect(contextAddedToTest.meta).to.deep.equal({ feature: 'reporting' })
})

it('should add context to normal test when not retried', function () {
// Create a normal (non-retried) CodeceptJS enhanced test
const normalTest = createTest('Normal test', () => {})

// Create a mock suite
const rootSuite = new MochaSuite('', null, true)
const suite = createSuite(rootSuite, 'Test Suite')
normalTest.addToSuite(suite)

// Simulate the test lifecycle hooks
helper._beforeSuite(suite)
helper._test(normalTest)

// Mock the _addContext method to capture what test object is passed
let contextAddedToTest = null
helper._addContext = function (testWrapper, context) {
contextAddedToTest = testWrapper.test
return Promise.resolve()
}

// Add some context using the helper
const contextData = { screenshot: 'normal.png' }
helper.addMochawesomeContext(contextData)

// The context should be added to the normal test
expect(contextAddedToTest).to.equal(normalTest)

// Verify this is not a retried test
expect(normalTest.retriedTest()).to.be.undefined
})
})
109 changes: 109 additions & 0 deletions test/unit/mocha/retry_integration_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
const { expect } = require('chai')
const { createTest } = require('../../../lib/mocha/test')
const { createSuite } = require('../../../lib/mocha/suite')
const MochaSuite = require('mocha/lib/suite')
const retryEnhancer = require('../../../lib/listener/retryEnhancer')
const event = require('../../../lib/event')

describe('Integration test: Retries with CodeceptJS properties', function () {
beforeEach(function () {
// Setup the retryEnhancer - this simulates what happens in CodeceptJS init
retryEnhancer()
})

it('should preserve all CodeceptJS properties during real retry scenario', function () {
// Create a test with retries like: Scenario().retries(2)
const originalTest = createTest('Test that might fail', () => {
throw new Error('Simulated failure')
})

// Set up test with various CodeceptJS properties that might be used in real scenarios
originalTest.opts = {
timeout: 30000,
metadata: 'important-test',
retries: 2,
feature: 'login',
}
originalTest.tags = ['@critical', '@smoke', '@login']
originalTest.notes = [
{ type: 'info', text: 'This test validates user login' },
{ type: 'warning', text: 'May be flaky due to external service' },
]
originalTest.meta = {
feature: 'authentication',
story: 'user-login',
priority: 'high',
team: 'qa',
}
originalTest.artifacts = ['login-screenshot.png', 'network-log.json']
originalTest.uid = 'auth-test-001'
originalTest.config = { helper: 'playwright', baseUrl: 'http://test.com' }
originalTest.inject = { userData: { email: 'test@example.com' } }

// Add some steps to simulate CodeceptJS test steps
originalTest.steps = [
{ title: 'I am on page "/login"', status: 'success' },
{ title: 'I fill field "email", "test@example.com"', status: 'success' },
{ title: 'I fill field "password", "secretpassword"', status: 'success' },
{ title: 'I click "Login"', status: 'failed' },
]

// Enable retries
originalTest.retries(2)

// Now simulate what happens during mocha retry
const retriedTest = originalTest.clone()

// Verify that the retried test has reference to original
expect(retriedTest.retriedTest()).to.equal(originalTest)

// Before our fix, these properties would be lost
expect(retriedTest.opts || {}).to.deep.equal({})
expect(retriedTest.tags || []).to.deep.equal([])

// Now trigger our retryEnhancer (this happens automatically in CodeceptJS)
event.emit(event.test.before, retriedTest)

// After our fix, all properties should be preserved
expect(retriedTest.opts).to.deep.equal({
timeout: 30000,
metadata: 'important-test',
retries: 2,
feature: 'login',
})
expect(retriedTest.tags).to.deep.equal(['@critical', '@smoke', '@login'])
expect(retriedTest.notes).to.deep.equal([
{ type: 'info', text: 'This test validates user login' },
{ type: 'warning', text: 'May be flaky due to external service' },
])
expect(retriedTest.meta).to.deep.equal({
feature: 'authentication',
story: 'user-login',
priority: 'high',
team: 'qa',
})
expect(retriedTest.artifacts).to.deep.equal(['login-screenshot.png', 'network-log.json'])
expect(retriedTest.uid).to.equal('auth-test-001')
expect(retriedTest.config).to.deep.equal({ helper: 'playwright', baseUrl: 'http://test.com' })
expect(retriedTest.inject).to.deep.equal({ userData: { email: 'test@example.com' } })
expect(retriedTest.steps).to.deep.equal([
{ title: 'I am on page "/login"', status: 'success' },
{ title: 'I fill field "email", "test@example.com"', status: 'success' },
{ title: 'I fill field "password", "secretpassword"', status: 'success' },
{ title: 'I click "Login"', status: 'failed' },
])

// Verify that enhanced methods are available
expect(retriedTest.addNote).to.be.a('function')
expect(retriedTest.applyOptions).to.be.a('function')
expect(retriedTest.simplify).to.be.a('function')

// Test that we can use the methods
retriedTest.addNote('retry', 'Attempt #2')
expect(retriedTest.notes).to.have.length(3)
expect(retriedTest.notes[2]).to.deep.equal({ type: 'retry', text: 'Attempt #2' })

// Verify the test is enhanced with CodeceptJS functionality
expect(retriedTest.codeceptjs).to.be.true
})
})
Loading