Skip to content

Commit 5598d39

Browse files
authored
Fix mocha retries losing CodeceptJS-specific properties (opts, tags, meta, etc.) (#5099)
1 parent 273a63e commit 5598d39

File tree

6 files changed

+413
-2
lines changed

6 files changed

+413
-2
lines changed

lib/codecept.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ class Codecept {
111111
runHook(require('./listener/helpers'))
112112
runHook(require('./listener/globalTimeout'))
113113
runHook(require('./listener/globalRetry'))
114+
runHook(require('./listener/retryEnhancer'))
114115
runHook(require('./listener/exit'))
115116
runHook(require('./listener/emptyRun'))
116117

lib/helper/Mochawesome.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,20 @@ class Mochawesome extends Helper {
3737
}
3838

3939
_test(test) {
40-
currentTest = { test }
40+
// If this is a retried test, we want to add context to the retried test
41+
// but also potentially preserve context from the original test
42+
const originalTest = test.retriedTest && test.retriedTest()
43+
if (originalTest) {
44+
// This is a retried test - use the retried test for context
45+
currentTest = { test }
46+
47+
// Optionally copy context from original test if it exists
48+
// Note: mochawesome context is stored in test.ctx, but we need to be careful
49+
// not to break the mocha context structure
50+
} else {
51+
// Normal test (not a retry)
52+
currentTest = { test }
53+
}
4154
}
4255

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

6578
addMochawesomeContext(context) {
6679
if (currentTest === '') currentTest = { test: currentSuite.ctx.test }
67-
return this._addContext(currentTest, context)
80+
81+
// For retried tests, make sure we're adding context to the current (retried) test
82+
// not the original test
83+
let targetTest = currentTest
84+
if (currentTest.test && currentTest.test.retriedTest && currentTest.test.retriedTest()) {
85+
// This test has been retried, make sure we're using the current test for context
86+
targetTest = { test: currentTest.test }
87+
}
88+
89+
return this._addContext(targetTest, context)
6890
}
6991
}
7092

lib/listener/retryEnhancer.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
const event = require('../event')
2+
const { enhanceMochaTest } = require('../mocha/test')
3+
4+
/**
5+
* Enhance retried tests by copying CodeceptJS-specific properties from the original test
6+
* This fixes the issue where Mocha's shallow clone during retries loses CodeceptJS properties
7+
*/
8+
module.exports = function () {
9+
event.dispatcher.on(event.test.before, test => {
10+
// Check if this test is a retry (has a reference to the original test)
11+
const originalTest = test.retriedTest && test.retriedTest()
12+
13+
if (originalTest) {
14+
// This is a retried test - copy CodeceptJS-specific properties from the original
15+
copyCodeceptJSProperties(originalTest, test)
16+
17+
// Ensure the test is enhanced with CodeceptJS functionality
18+
enhanceMochaTest(test)
19+
}
20+
})
21+
}
22+
23+
/**
24+
* Copy CodeceptJS-specific properties from the original test to the retried test
25+
* @param {CodeceptJS.Test} originalTest - The original test object
26+
* @param {CodeceptJS.Test} retriedTest - The retried test object
27+
*/
28+
function copyCodeceptJSProperties(originalTest, retriedTest) {
29+
// Copy CodeceptJS-specific properties
30+
if (originalTest.opts !== undefined) {
31+
retriedTest.opts = originalTest.opts ? { ...originalTest.opts } : {}
32+
}
33+
34+
if (originalTest.tags !== undefined) {
35+
retriedTest.tags = originalTest.tags ? [...originalTest.tags] : []
36+
}
37+
38+
if (originalTest.notes !== undefined) {
39+
retriedTest.notes = originalTest.notes ? [...originalTest.notes] : []
40+
}
41+
42+
if (originalTest.meta !== undefined) {
43+
retriedTest.meta = originalTest.meta ? { ...originalTest.meta } : {}
44+
}
45+
46+
if (originalTest.artifacts !== undefined) {
47+
retriedTest.artifacts = originalTest.artifacts ? [...originalTest.artifacts] : []
48+
}
49+
50+
if (originalTest.steps !== undefined) {
51+
retriedTest.steps = originalTest.steps ? [...originalTest.steps] : []
52+
}
53+
54+
if (originalTest.config !== undefined) {
55+
retriedTest.config = originalTest.config ? { ...originalTest.config } : {}
56+
}
57+
58+
if (originalTest.inject !== undefined) {
59+
retriedTest.inject = originalTest.inject ? { ...originalTest.inject } : {}
60+
}
61+
62+
// Copy methods that might be missing
63+
if (originalTest.addNote && !retriedTest.addNote) {
64+
retriedTest.addNote = function (type, note) {
65+
this.notes = this.notes || []
66+
this.notes.push({ type, text: note })
67+
}
68+
}
69+
70+
if (originalTest.applyOptions && !retriedTest.applyOptions) {
71+
retriedTest.applyOptions = originalTest.applyOptions.bind(retriedTest)
72+
}
73+
74+
if (originalTest.simplify && !retriedTest.simplify) {
75+
retriedTest.simplify = originalTest.simplify.bind(retriedTest)
76+
}
77+
78+
// Preserve the uid if it exists
79+
if (originalTest.uid !== undefined) {
80+
retriedTest.uid = originalTest.uid
81+
}
82+
83+
// Mark as enhanced
84+
retriedTest.codeceptjs = true
85+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
const { expect } = require('chai')
2+
const { createTest } = require('../../../lib/mocha/test')
3+
const { createSuite } = require('../../../lib/mocha/suite')
4+
const MochaSuite = require('mocha/lib/suite')
5+
const Test = require('mocha/lib/test')
6+
const Mochawesome = require('../../../lib/helper/Mochawesome')
7+
const retryEnhancer = require('../../../lib/listener/retryEnhancer')
8+
const event = require('../../../lib/event')
9+
10+
describe('MochawesomeHelper with retries', function () {
11+
let helper
12+
13+
beforeEach(function () {
14+
helper = new Mochawesome({})
15+
// Setup the retryEnhancer
16+
retryEnhancer()
17+
})
18+
19+
it('should add context to the correct test object when test is retried', function () {
20+
// Create a CodeceptJS enhanced test
21+
const originalTest = createTest('Test with mochawesome context', () => {})
22+
23+
// Create a mock suite and set up context
24+
const rootSuite = new MochaSuite('', null, true)
25+
const suite = createSuite(rootSuite, 'Test Suite')
26+
originalTest.addToSuite(suite)
27+
28+
// Set some CodeceptJS-specific properties
29+
originalTest.opts = { timeout: 5000 }
30+
originalTest.meta = { feature: 'reporting' }
31+
32+
// Simulate what happens during mocha retries - using mocha's native clone method
33+
const retriedTest = Test.prototype.clone.call(originalTest)
34+
35+
// Trigger the retryEnhancer to copy properties
36+
event.emit(event.test.before, retriedTest)
37+
38+
// Verify that properties were copied
39+
expect(retriedTest.opts).to.deep.equal({ timeout: 5000 })
40+
expect(retriedTest.meta).to.deep.equal({ feature: 'reporting' })
41+
42+
// Now simulate the test lifecycle hooks
43+
helper._beforeSuite(suite)
44+
helper._test(retriedTest) // This should set currentTest to the retried test
45+
46+
// Add some context using the helper
47+
const contextData = { screenshot: 'test.png', url: 'http://example.com' }
48+
49+
// Mock the _addContext method to capture what test object is passed
50+
let contextAddedToTest = null
51+
helper._addContext = function (testWrapper, context) {
52+
contextAddedToTest = testWrapper.test
53+
return Promise.resolve()
54+
}
55+
56+
// Add context
57+
helper.addMochawesomeContext(contextData)
58+
59+
// The context should be added to the retried test, not the original
60+
expect(contextAddedToTest).to.equal(retriedTest)
61+
expect(contextAddedToTest).to.not.equal(originalTest)
62+
63+
// Verify the retried test has the enhanced properties
64+
expect(contextAddedToTest.opts).to.deep.equal({ timeout: 5000 })
65+
expect(contextAddedToTest.meta).to.deep.equal({ feature: 'reporting' })
66+
})
67+
68+
it('should add context to normal test when not retried', function () {
69+
// Create a normal (non-retried) CodeceptJS enhanced test
70+
const normalTest = createTest('Normal test', () => {})
71+
72+
// Create a mock suite
73+
const rootSuite = new MochaSuite('', null, true)
74+
const suite = createSuite(rootSuite, 'Test Suite')
75+
normalTest.addToSuite(suite)
76+
77+
// Simulate the test lifecycle hooks
78+
helper._beforeSuite(suite)
79+
helper._test(normalTest)
80+
81+
// Mock the _addContext method to capture what test object is passed
82+
let contextAddedToTest = null
83+
helper._addContext = function (testWrapper, context) {
84+
contextAddedToTest = testWrapper.test
85+
return Promise.resolve()
86+
}
87+
88+
// Add some context using the helper
89+
const contextData = { screenshot: 'normal.png' }
90+
helper.addMochawesomeContext(contextData)
91+
92+
// The context should be added to the normal test
93+
expect(contextAddedToTest).to.equal(normalTest)
94+
95+
// Verify this is not a retried test
96+
expect(normalTest.retriedTest()).to.be.undefined
97+
})
98+
})
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
const { expect } = require('chai')
2+
const { createTest } = require('../../../lib/mocha/test')
3+
const { createSuite } = require('../../../lib/mocha/suite')
4+
const MochaSuite = require('mocha/lib/suite')
5+
const retryEnhancer = require('../../../lib/listener/retryEnhancer')
6+
const event = require('../../../lib/event')
7+
8+
describe('Integration test: Retries with CodeceptJS properties', function () {
9+
beforeEach(function () {
10+
// Setup the retryEnhancer - this simulates what happens in CodeceptJS init
11+
retryEnhancer()
12+
})
13+
14+
it('should preserve all CodeceptJS properties during real retry scenario', function () {
15+
// Create a test with retries like: Scenario().retries(2)
16+
const originalTest = createTest('Test that might fail', () => {
17+
throw new Error('Simulated failure')
18+
})
19+
20+
// Set up test with various CodeceptJS properties that might be used in real scenarios
21+
originalTest.opts = {
22+
timeout: 30000,
23+
metadata: 'important-test',
24+
retries: 2,
25+
feature: 'login',
26+
}
27+
originalTest.tags = ['@critical', '@smoke', '@login']
28+
originalTest.notes = [
29+
{ type: 'info', text: 'This test validates user login' },
30+
{ type: 'warning', text: 'May be flaky due to external service' },
31+
]
32+
originalTest.meta = {
33+
feature: 'authentication',
34+
story: 'user-login',
35+
priority: 'high',
36+
team: 'qa',
37+
}
38+
originalTest.artifacts = ['login-screenshot.png', 'network-log.json']
39+
originalTest.uid = 'auth-test-001'
40+
originalTest.config = { helper: 'playwright', baseUrl: 'http://test.com' }
41+
originalTest.inject = { userData: { email: '[email protected]' } }
42+
43+
// Add some steps to simulate CodeceptJS test steps
44+
originalTest.steps = [
45+
{ title: 'I am on page "/login"', status: 'success' },
46+
{ title: 'I fill field "email", "[email protected]"', status: 'success' },
47+
{ title: 'I fill field "password", "secretpassword"', status: 'success' },
48+
{ title: 'I click "Login"', status: 'failed' },
49+
]
50+
51+
// Enable retries
52+
originalTest.retries(2)
53+
54+
// Now simulate what happens during mocha retry
55+
const retriedTest = originalTest.clone()
56+
57+
// Verify that the retried test has reference to original
58+
expect(retriedTest.retriedTest()).to.equal(originalTest)
59+
60+
// Before our fix, these properties would be lost
61+
expect(retriedTest.opts || {}).to.deep.equal({})
62+
expect(retriedTest.tags || []).to.deep.equal([])
63+
64+
// Now trigger our retryEnhancer (this happens automatically in CodeceptJS)
65+
event.emit(event.test.before, retriedTest)
66+
67+
// After our fix, all properties should be preserved
68+
expect(retriedTest.opts).to.deep.equal({
69+
timeout: 30000,
70+
metadata: 'important-test',
71+
retries: 2,
72+
feature: 'login',
73+
})
74+
expect(retriedTest.tags).to.deep.equal(['@critical', '@smoke', '@login'])
75+
expect(retriedTest.notes).to.deep.equal([
76+
{ type: 'info', text: 'This test validates user login' },
77+
{ type: 'warning', text: 'May be flaky due to external service' },
78+
])
79+
expect(retriedTest.meta).to.deep.equal({
80+
feature: 'authentication',
81+
story: 'user-login',
82+
priority: 'high',
83+
team: 'qa',
84+
})
85+
expect(retriedTest.artifacts).to.deep.equal(['login-screenshot.png', 'network-log.json'])
86+
expect(retriedTest.uid).to.equal('auth-test-001')
87+
expect(retriedTest.config).to.deep.equal({ helper: 'playwright', baseUrl: 'http://test.com' })
88+
expect(retriedTest.inject).to.deep.equal({ userData: { email: '[email protected]' } })
89+
expect(retriedTest.steps).to.deep.equal([
90+
{ title: 'I am on page "/login"', status: 'success' },
91+
{ title: 'I fill field "email", "[email protected]"', status: 'success' },
92+
{ title: 'I fill field "password", "secretpassword"', status: 'success' },
93+
{ title: 'I click "Login"', status: 'failed' },
94+
])
95+
96+
// Verify that enhanced methods are available
97+
expect(retriedTest.addNote).to.be.a('function')
98+
expect(retriedTest.applyOptions).to.be.a('function')
99+
expect(retriedTest.simplify).to.be.a('function')
100+
101+
// Test that we can use the methods
102+
retriedTest.addNote('retry', 'Attempt #2')
103+
expect(retriedTest.notes).to.have.length(3)
104+
expect(retriedTest.notes[2]).to.deep.equal({ type: 'retry', text: 'Attempt #2' })
105+
106+
// Verify the test is enhanced with CodeceptJS functionality
107+
expect(retriedTest.codeceptjs).to.be.true
108+
})
109+
})

0 commit comments

Comments
 (0)