Skip to content

Commit acdd13c

Browse files
author
DavertMik
committed
Improved bdd tests
1 parent 29566b5 commit acdd13c

File tree

9 files changed

+137
-55
lines changed

9 files changed

+137
-55
lines changed

examples/codecept.config.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ export const config = {
3737
steps: ['./step_definitions/steps.js'],
3838
},
3939
plugins: {
40-
analyze: {
41-
enabled: true,
42-
},
40+
// analyze: {
41+
// enabled: true,
42+
// },
4343
// heal: {
4444
// enabled: true,
4545
// },

lib/codecept.js

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const __filename = fileURLToPath(import.meta.url)
1010
const __dirname = dirname(__filename)
1111
const require = createRequire(import.meta.url)
1212

13-
import Helper from '@codeceptjs/helper';
13+
import Helper from '@codeceptjs/helper'
1414
import container from './container.js'
1515
import Config from './config.js'
1616
import event from './event.js'
@@ -92,21 +92,21 @@ class Codecept {
9292
/**
9393
* Executes hooks.
9494
*/
95-
async runHooks() {
96-
// default hooks
97-
runHook(storeListener)
98-
runHook(stepsListener)
99-
runHook(configListener)
100-
runHook(resultListener)
101-
runHook(helpersListener)
102-
runHook(globalTimeoutListener)
103-
runHook(globalRetryListener)
104-
runHook(exitListener)
105-
runHook(emptyRunListener)
106-
107-
// custom hooks (previous iteration of plugins)
108-
this.config.hooks.forEach(hook => runHook(hook))
109-
}
95+
async runHooks() {
96+
// default hooks
97+
runHook(storeListener)
98+
runHook(stepsListener)
99+
runHook(configListener)
100+
runHook(resultListener)
101+
runHook(helpersListener)
102+
runHook(globalTimeoutListener)
103+
runHook(globalRetryListener)
104+
runHook(exitListener)
105+
runHook(emptyRunListener)
106+
107+
// custom hooks (previous iteration of plugins)
108+
this.config.hooks.forEach(hook => runHook(hook))
109+
}
110110

111111
/**
112112
* Executes bootstrap.
@@ -181,7 +181,7 @@ class Codecept {
181181
*/
182182
async run(test) {
183183
await container.started()
184-
184+
185185
// Ensure translations are loaded for Gherkin features
186186
try {
187187
const { loadTranslations } = await import('./mocha/gherkin.js')
@@ -193,12 +193,18 @@ class Codecept {
193193
return new Promise((resolve, reject) => {
194194
const mocha = container.mocha()
195195
mocha.files = this.testFiles
196+
196197
if (test) {
197198
if (!fsPath.isAbsolute(test)) {
198199
test = fsPath.join(global.codecept_dir, test)
199200
}
200-
mocha.files = mocha.files.filter(t => fsPath.basename(t, '.js') === test || t === test)
201+
const testBasename = fsPath.basename(test, '.js')
202+
const testFeatureBasename = fsPath.basename(test, '.feature')
203+
mocha.files = mocha.files.filter(t => {
204+
return fsPath.basename(t, '.js') === testBasename || fsPath.basename(t, '.feature') === testFeatureBasename || t === test
205+
})
201206
}
207+
202208
const done = () => {
203209
event.emit(event.all.result, container.result())
204210
event.emit(event.all.after, this)

lib/helper/Playwright.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,9 @@ class Playwright extends Helper {
493493
}
494494

495495
_beforeSuite() {
496-
if ((restartsSession() || restartsContext()) && !this.options.manualStart && !this.isRunning) {
496+
// Start browser if not manually started and not already running
497+
// Browser should start in singleton mode (restart: false) or when restart strategy is enabled
498+
if (!this.options.manualStart && !this.isRunning) {
497499
this.debugSection('Session', 'Starting singleton browser session')
498500
return this._startBrowser()
499501
}
@@ -650,10 +652,10 @@ class Playwright extends Helper {
650652
}
651653

652654
async _afterSuite() {
653-
// Ensure proper browser cleanup after test suite
654-
if (this.isRunning) {
655+
// Only stop browser if restart strategy requires it
656+
if ((restartsSession() || restartsContext() || restartsBrowser()) && this.isRunning) {
655657
try {
656-
await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout')), 15000))])
658+
await this._stopBrowser()
657659
} catch (e) {
658660
console.warn('Warning during suite cleanup:', e.message)
659661
// Track suite cleanup failures
@@ -748,6 +750,17 @@ class Playwright extends Helper {
748750
}
749751
}
750752

753+
async _cleanup() {
754+
// Final cleanup when test run completes
755+
if (this.isRunning) {
756+
try {
757+
await this._stopBrowser()
758+
} catch (e) {
759+
console.warn('Warning during final cleanup:', e.message)
760+
}
761+
}
762+
}
763+
751764
_session() {
752765
const defaultContext = this.browserContext
753766
return {
@@ -1091,16 +1104,16 @@ class Playwright extends Helper {
10911104
}
10921105

10931106
try {
1094-
if (this.options.recordHar && this.browserContext) {
1095-
await Promise.race([this.browserContext.close(), new Promise((_, reject) => setTimeout(() => reject(new Error('Context close timeout')), 5000))])
1107+
if (this.browserContext) {
1108+
await Promise.race([this.browserContext.close(), new Promise((_, reject) => setTimeout(() => reject(new Error('Context close timeout')), 3000))])
10961109
}
10971110
} catch (error) {
10981111
console.warn('Failed to close browser context:', error.message)
10991112
}
11001113

11011114
try {
11021115
if (this.browser) {
1103-
await Promise.race([this.browser.close(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser close timeout')), 10000))])
1116+
await Promise.race([this.browser.close(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser close timeout')), 5000))])
11041117
}
11051118
} catch (error) {
11061119
console.warn('Failed to close browser:', error.message)

lib/mocha/factory.js

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,28 +39,34 @@ class MochaFactory {
3939
process.exit(1)
4040
}
4141

42-
mocha.loadFiles = fn => {
42+
// Override loadFiles to handle feature files
43+
const originalLoadFiles = Mocha.prototype.loadFiles
44+
mocha.loadFiles = function (fn) {
4345
// load features
44-
if (mocha.suite.suites.length === 0) {
45-
const featureFiles = mocha.files.filter(file => file.match(/\.feature$/))
46+
const featureFiles = this.files.filter(file => file.match(/\.feature$/))
47+
if (featureFiles.length > 0) {
48+
// Load translations for Gherkin features
49+
loadTranslations().catch(() => {
50+
// Ignore if translations can't be loaded
51+
})
52+
4653
for (const file of featureFiles) {
4754
const suite = gherkinParser(fs.readFileSync(file, 'utf8'), file)
48-
mocha.suite.addSuite(suite)
55+
this.suite.addSuite(suite)
4956
}
5057

5158
// remove feature files
52-
const jsFiles = mocha.files.filter(file => !file.match(/\.feature$/))
53-
mocha.files = mocha.files.filter(file => !file.match(/\.feature$/))
59+
const jsFiles = this.files.filter(file => !file.match(/\.feature$/))
60+
this.files = this.files.filter(file => !file.match(/\.feature$/))
5461

5562
// Load JavaScript test files using ESM imports
5663
if (jsFiles.length > 0) {
5764
try {
5865
// Try original loadFiles first for compatibility
59-
Mocha.prototype.loadFiles.call(mocha, fn)
66+
originalLoadFiles.call(this, fn)
6067
} catch (e) {
6168
// If original loadFiles fails, load ESM files manually
6269
if (e.message.includes('not in cache') || e.message.includes('ESM') || e.message.includes('getStatus')) {
63-
console.warn('Loading ESM test files manually due to Mocha compatibility issues')
6470
// Load ESM files by importing them synchronously using top-level await workaround
6571
for (const file of jsFiles) {
6672
try {
@@ -71,8 +77,10 @@ class MochaFactory {
7177
// If dynamic import fails, the file may have syntax errors or other issues
7278
console.error(`Failed to load test file ${file}:`, importErr.message)
7379
})
80+
if (fn) fn()
7481
} catch (fileErr) {
7582
console.error(`Error processing test file ${file}:`, fileErr.message)
83+
if (fn) fn(fileErr)
7684
}
7785
}
7886
} else {
@@ -85,7 +93,7 @@ class MochaFactory {
8593
const dupes = []
8694
let missingFeatureInFile = []
8795
const seenTests = []
88-
mocha.suite.eachTest(test => {
96+
this.suite.eachTest(test => {
8997
if (!test) {
9098
return // Skip undefined tests
9199
}
@@ -108,6 +116,9 @@ class MochaFactory {
108116
missingFeatureInFile = [...new Set(missingFeatureInFile)]
109117
output.error(`Missing Feature section in:\n${missingFeatureInFile.join('\n')}`)
110118
}
119+
} else {
120+
// Use original for non-feature files
121+
originalLoadFiles.call(this, fn)
111122
}
112123
}
113124

lib/mocha/gherkin.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const gherkinParser = (text, file) => {
2525
let currentLanguage
2626

2727
if (ast.feature) {
28+
// Ensure translations are loaded before trying to access them
2829
currentLanguage = getTranslation(ast.feature.language)
2930
}
3031

@@ -41,10 +42,16 @@ const gherkinParser = (text, file) => {
4142
suite.file = file
4243
suite.timeout(0)
4344

44-
suite.beforeEach('codeceptjs.before', () => setup(suite))
45-
suite.afterEach('codeceptjs.after', () => teardown(suite))
46-
suite.beforeAll('codeceptjs.beforeSuite', () => suiteSetup(suite)())
47-
suite.afterAll('codeceptjs.afterSuite', () => suiteTeardown(suite))
45+
suite.beforeEach('codeceptjs.before', function () {
46+
// In Mocha, 'this' refers to the current test in beforeEach/afterEach hooks
47+
setup(this)(() => {})
48+
})
49+
suite.afterEach('codeceptjs.after', function () {
50+
// In Mocha, 'this' refers to the current test in beforeEach/afterEach hooks
51+
teardown(this)(() => {})
52+
})
53+
suite.beforeAll('codeceptjs.beforeSuite', suiteSetup(suite))
54+
suite.afterAll('codeceptjs.afterSuite', suiteTeardown(suite))
4855

4956
const runSteps = async steps => {
5057
for (const step of steps) {
@@ -190,6 +197,11 @@ function addExampleInTable(exampleSteps, placeholders) {
190197
let translations = null
191198
async function loadTranslations() {
192199
if (!translations) {
200+
// Import container to ensure it's initialized
201+
const Container = await import('../container.js')
202+
await Container.default.started()
203+
204+
// Now load translations
193205
const translationsModule = await import('../../translations/index.js')
194206
translations = translationsModule.default || translationsModule
195207
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/// <reference types='codeceptjs' />
2+
type steps_file = (typeof import('../../support/custom_steps.js'))['default']
3+
type MyPage = (typeof import('../../support/my_page.js'))['default']
4+
type SecondPage = (typeof import('../../support/second_page.js'))['default']
5+
type CurrentPage = (typeof import('./po/custom_steps.js'))['default']
6+
7+
declare namespace CodeceptJS {
8+
interface SupportObject {
9+
I: I
10+
current: any
11+
MyPage: MyPage
12+
SecondPage: SecondPage
13+
CurrentPage: CurrentPage
14+
}
15+
interface Methods extends FileSystem {}
16+
interface I extends ReturnType<steps_file>, WithTranslation<Methods> {}
17+
namespace Translation {
18+
interface Actions {}
19+
}
20+
}

test/data/sandbox/features/step_definitions/my_steps.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { actor } from '../../../../../lib/index.js'
22
import { secret } from '../../../../../lib/secret.js'
3-
import { Given, When, Then } from '../../../../../lib/mocha/bdd.js'
3+
import { Given, When, Then, Before, After, Fail } from '../../../../../lib/mocha/bdd.js'
44

55
Given(/I have product with \$(\d+) price/, price => {
66
const I = actor()
77
I.addItem(parseInt(price, 10))
88
})
9+
10+
Given('I have product with {int} price in my cart', price => {
11+
const I = actor()
12+
I.addItem(parseInt(price, 10))
13+
})
914
When('I go to checkout process', () => {
1015
const I = actor()
1116
I.checkout()
@@ -16,9 +21,9 @@ Then('I should see that total number of products is {int}', num => {
1621
const I = actor()
1722
I.seeNum(num)
1823
})
19-
Then('my order amount is ${int}', sum => {
24+
Then(/my order amount is \$(\d+)/, sum => {
2025
const I = actor()
21-
I.seeSum(sum)
26+
I.seeSum(parseInt(sum, 10))
2227
})
2328

2429
Given('I have product with price {int}$ in my cart', price => {

test/data/sandbox/support/bdd_helper.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,18 @@ import helperModule from '../../../../lib/helper.js'
33
const Helper = helperModule.default || helperModule
44

55
class CheckoutHelper extends Helper {
6+
constructor(config) {
7+
super(config)
8+
console.log('DEBUG: CheckoutHelper constructor called')
9+
}
10+
11+
_init() {
12+
console.log('DEBUG: CheckoutHelper._init called')
13+
}
14+
615
_before() {
16+
console.log('DEBUG: CheckoutHelper._before called, resetting state')
17+
console.log('DEBUG: Current state before reset:', { num: this.num, sum: this.sum })
718
this.num = 0
819
this.sum = 0
920
this.discountCalc = null

test/runner/bdd_test.js

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
import chai from 'chai';
2-
chai.should();
3-
import assert from 'assert';
4-
import path from 'path';
5-
import { exec } from 'child_process';
6-
import { fileURLToPath } from 'url';
7-
8-
const __filename = fileURLToPath(import.meta.url);
9-
const __dirname = path.dirname(__filename);
1+
import chai from 'chai'
2+
chai.should()
3+
import assert from 'assert'
4+
import path from 'path'
5+
import { exec } from 'child_process'
6+
import { fileURLToPath } from 'url'
107

8+
const __filename = fileURLToPath(import.meta.url)
9+
const __dirname = path.dirname(__filename)
1110

1211
const runner = path.join(__dirname, '/../../bin/codecept.js')
1312
const codecept_dir = path.join(__dirname, '/../data/sandbox')
@@ -20,7 +19,7 @@ describe('BDD Gherkin', () => {
2019
})
2120

2221
it('should run feature files', done => {
23-
exec(config_run_config('codecept.bdd.js') + ' --steps --grep "Checkout process"', (err, stdout, stderr) => {
22+
const child = exec(config_run_config('codecept.bdd.js') + ' --steps --grep "Checkout process"', { timeout: 10000 }, (err, stdout, stderr) => {
2423
console.log('=== ACTUAL OUTPUT ===')
2524
console.log(stdout)
2625
console.log('=== STDERR ===')
@@ -38,9 +37,14 @@ describe('BDD Gherkin', () => {
3837
stdout.should.include('And my order amount is $1600')
3938
stdout.should.not.include('I add item 600') // 'Given' actor's non-gherkin step check
4039
stdout.should.not.include('I see sum 1600') // 'And' actor's non-gherkin step check
41-
assert(!err)
4240
done()
4341
})
42+
43+
child.on('timeout', () => {
44+
console.error('Test timed out')
45+
child.kill()
46+
done(new Error('Test timed out'))
47+
})
4448
})
4549

4650
it('should print substeps in debug mode', done => {

0 commit comments

Comments
 (0)