Skip to content

Commit de60cf6

Browse files
committed
fix issues
1 parent 40dc1cb commit de60cf6

File tree

6 files changed

+105
-54
lines changed

6 files changed

+105
-54
lines changed

lib/container.js

Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,44 @@ class Container {
6868
container.plugins = await createPlugins(config.plugins || {}, opts)
6969
container.result = new Result()
7070

71-
createActor(config.include?.I)
71+
// Preload includes (so proxies can expose real objects synchronously)
72+
const includes = config.include || {}
73+
74+
// Ensure I is available for DI modules at import time
75+
if (Object.prototype.hasOwnProperty.call(includes, 'I')) {
76+
try {
77+
const mod = includes.I
78+
if (typeof mod === 'string') {
79+
container.support.I = await loadSupportObject(mod, 'I')
80+
} else if (typeof mod === 'function') {
81+
container.support.I = await loadSupportObject(mod, 'I')
82+
} else if (mod && typeof mod === 'object') {
83+
container.support.I = mod
84+
}
85+
} catch (e) {
86+
throw new Error(`Could not include object I: ${e.message}`)
87+
}
88+
} else {
89+
// Create default actor if not provided via includes
90+
createActor()
91+
}
92+
93+
// Load remaining includes except I
94+
for (const [name, mod] of Object.entries(includes)) {
95+
if (name === 'I') continue
96+
try {
97+
if (typeof mod === 'string') {
98+
container.support[name] = await loadSupportObject(mod, name)
99+
} else if (typeof mod === 'function') {
100+
// function or class
101+
container.support[name] = await loadSupportObject(mod, name)
102+
} else if (mod && typeof mod === 'object') {
103+
container.support[name] = mod
104+
}
105+
} catch (e) {
106+
throw new Error(`Could not include object ${name}: ${e.message}`)
107+
}
108+
}
72109

73110
if (opts && opts.ai) ai.enable(config.ai) // enable AI Assistant
74111
if (config.gherkin) await loadGherkinStepsAsync(config.gherkin.steps || [])
@@ -228,7 +265,7 @@ function createHelpers(config) {
228265
}
229266

230267
// ESM import (legacy check)
231-
if (!HelperClass && helperName?.constructor === Function && helperName.prototype) {
268+
if (!HelperClass && typeof helperName === 'function' && helperName.prototype) {
232269
HelperClass = helperName
233270
helperName = HelperClass.constructor.name
234271
}
@@ -445,7 +482,7 @@ function createSupportObjects(config) {
445482
return {
446483
enumerable: true,
447484
configurable: true,
448-
value: this.get(target, prop),
485+
value: container.support[name][prop],
449486
}
450487
},
451488
ownKeys() {
@@ -472,10 +509,21 @@ function createSupportObjects(config) {
472509
return {
473510
enumerable: true,
474511
configurable: true,
475-
value: this.get(target, prop),
512+
value: target[prop],
476513
}
477514
},
478515
get(target, key) {
516+
if (typeof key === 'symbol') {
517+
// safely ignore symbol-based meta properties used by tooling
518+
return undefined
519+
}
520+
// Allow special I even if not declared in includes
521+
if (key === 'I') {
522+
return lazyLoad('I')
523+
}
524+
if (!keys.includes(key)) {
525+
throw new Error(`Support object "${String(key)}" is not defined`)
526+
}
479527
return lazyLoad(key)
480528
},
481529
},
@@ -485,12 +533,8 @@ function createSupportObjects(config) {
485533
function createActor(actorPath) {
486534
if (container.support.I) return container.support.I
487535

488-
if (actorPath) {
489-
// Actor path loading must be handled async during container creation
490-
throw new Error(`Custom actor loading from path '${actorPath}' must be handled asynchronously during container creation`)
491-
} else {
492-
container.support.I = actorFactory({}, Container)
493-
}
536+
// Default actor
537+
container.support.I = actorFactory({}, Container)
494538

495539
return container.support.I
496540
}
@@ -508,7 +552,7 @@ async function loadPluginAsync(modulePath, config) {
508552
if (typeof pluginFactory !== 'function') {
509553
throw new Error(`Plugin '${modulePath}' is not a function. Expected a plugin factory function.`)
510554
}
511-
555+
512556
return pluginFactory(config)
513557
}
514558

@@ -537,7 +581,7 @@ async function createPlugins(config, options = {}) {
537581
} else {
538582
module = `./plugin/${pluginName}.js`
539583
}
540-
584+
541585
// Use async loading for all plugins (ESM and CJS)
542586
plugins[pluginName] = await loadPluginAsync(module, config[pluginName])
543587
debug(`plugin ${pluginName} loaded via async import`)
@@ -592,12 +636,31 @@ async function loadSupportObject(modulePath, supportObjectName) {
592636
if (!modulePath) {
593637
throw new Error(`Support object "${supportObjectName}" is not defined`)
594638
}
595-
if (modulePath.charAt(0) === '.') {
639+
// If function/class provided directly
640+
if (typeof modulePath === 'function') {
641+
try {
642+
// class constructor
643+
if (modulePath.prototype && modulePath.prototype.constructor === modulePath) {
644+
return new modulePath()
645+
}
646+
// plain function factory
647+
return modulePath()
648+
} catch (err) {
649+
throw new Error(`Could not include object ${supportObjectName} from function: ${err.message}`)
650+
}
651+
}
652+
if (typeof modulePath === 'string' && modulePath.charAt(0) === '.') {
596653
modulePath = path.join(global.codecept_dir, modulePath)
597654
}
598655
try {
599656
// Use dynamic import for both ESM and CJS modules
600-
const obj = await import(modulePath)
657+
let importPath = modulePath
658+
// Append .js if no extension provided (ESM resolution requires it)
659+
if (typeof importPath === 'string') {
660+
const ext = path.extname(importPath)
661+
if (!ext) importPath = `${importPath}.js`
662+
}
663+
const obj = await import(importPath)
601664

602665
// Handle ESM module wrapper
603666
let actualObj = obj

lib/effects.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ async function hopeThat(callback) {
132132
const msg = err.inspect ? err.inspect() : err.toString()
133133
output.debug(`Unsuccessful assertion > ${msg}`)
134134
event.dispatcher.once(event.test.finished, test => {
135+
if (!test.notes) test.notes = []
135136
test.notes.push({ type: 'conditionalError', text: msg })
136137
})
137138
recorder.session.restore(sessionName)

lib/els.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import container from './container.js'
44
import StepConfig from './step/config.js'
55
import recordStep from './step/record.js'
66
import FuncStep from './step/func.js'
7-
import truth from './assert/truth.js'
7+
import { truth } from './assert/truth.js'
88
import { isAsyncFunction, humanizeFunction } from './utils.js'
99

1010
function element(purpose, locator, fn) {

lib/mocha/asyncWrapper.js

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ function test(test) {
5454

5555
test.fn = function (done) {
5656
const doneFn = makeDoneCallableOnce(done)
57+
// Ensure recorder is running so any steps added inside test function are executed
58+
recorder.startUnlessRunning()
5759
recorder.errHandler(err => {
5860
recorder.session.start('teardown')
5961
recorder.cleanAsyncErr()
@@ -74,40 +76,26 @@ function test(test) {
7476
event.emit(event.test.finished, test)
7577
recorder.add(() => doneFn(err))
7678
})
77-
78-
if (isAsyncFunction(testFn)) {
79-
event.emit(event.test.started, test)
80-
getInjectedArguments(testFn, test).then(args => {
81-
testFn
82-
.call(test, args)
83-
.then(() => {
84-
recorder.add('fire test.passed', () => {
85-
event.emit(event.test.passed, test)
86-
event.emit(event.test.finished, test)
87-
})
88-
recorder.add('finish test', doneFn)
89-
})
90-
.catch(err => {
91-
recorder.throw(err)
92-
})
93-
.finally(() => {
94-
recorder.catch()
95-
})
96-
})
97-
return
98-
}
99-
100-
getInjectedArguments(testFn, test).then(args => {
79+
// Schedule the test execution inside recorder to ensure any external recorder.add
80+
// calls happen after this and steps added within testFn are executed before finishing
81+
recorder.add('execute test function', async () => {
10182
try {
83+
const args = await getInjectedArguments(testFn, test)
10284
event.emit(event.test.started, test)
103-
testFn.call(test, args)
104-
} catch (err) {
105-
recorder.throw(err)
106-
} finally {
85+
if (isAsyncFunction(testFn)) {
86+
await testFn.call(test, args)
87+
} else {
88+
testFn.call(test, args)
89+
}
90+
// After testFn schedules its own steps, enqueue passed/finished
10791
recorder.add('fire test.passed', () => {
10892
event.emit(event.test.passed, test)
10993
event.emit(event.test.finished, test)
11094
})
95+
} catch (err) {
96+
recorder.throw(err)
97+
} finally {
98+
// Finish test after queued tasks
11199
recorder.add('finish test', doneFn)
112100
recorder.catch()
113101
}

lib/mocha/ui.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export default function (suite) {
4545
let afterEachHooksAreLoaded
4646

4747
suite.on('pre-require', (context, file, mocha) => {
48-
common(suites, context, mocha)
48+
const cmn = common(suites, context, mocha)
4949

5050
const addScenario = function (title, opts = {}, fn) {
5151
const suite = suites[0]
@@ -67,8 +67,8 @@ export default function (suite) {
6767

6868
// create dispatcher
6969

70-
context.BeforeAll = common.before
71-
context.AfterAll = common.after
70+
context.BeforeAll = cmn.before
71+
context.AfterAll = cmn.after
7272

7373
context.run = mocha.options.delay && common.runWithSuite(suite)
7474
/**

test/unit/container_test.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ describe('Container', () => {
2525
})
2626

2727
describe('#translation', () => {
28-
2928
it('should create empty translation', async () => {
3029
await container.create({})
3130
expect(container.translation()).to.be.instanceOf(Translation)
@@ -158,7 +157,7 @@ describe('Container', () => {
158157
const config = {
159158
helpers: {
160159
MyHelper: {
161-
require: './data/helper',
160+
require: './data/helper.js',
162161
},
163162
FileSystem: {},
164163
},
@@ -182,7 +181,7 @@ describe('Container', () => {
182181
it('should load DI and return a reference to the module', async () => {
183182
await container.create({
184183
include: {
185-
dummyPage: './data/dummy_page',
184+
dummyPage: './data/dummy_page.js',
186185
},
187186
})
188187
const dummyPage = await import('../data/dummy_page.js')
@@ -192,7 +191,7 @@ describe('Container', () => {
192191
it('should load I from path and execute', async () => {
193192
await container.create({
194193
include: {
195-
I: './data/I',
194+
I: './data/I.js',
196195
},
197196
})
198197
expect(container.support('I')).is.ok
@@ -214,8 +213,8 @@ describe('Container', () => {
214213
it('should load DI and inject I into PO', async () => {
215214
await container.create({
216215
include: {
217-
dummyPage: './data/dummy_page',
218-
I: './data/I',
216+
dummyPage: './data/dummy_page.js',
217+
I: './data/I.js',
219218
},
220219
})
221220
expect(container.support('dummyPage')).is.ok
@@ -227,8 +226,8 @@ describe('Container', () => {
227226
it('should load DI and inject custom I into PO', async () => {
228227
await container.create({
229228
include: {
230-
dummyPage: './data/dummy_page',
231-
I: './data/I',
229+
dummyPage: './data/dummy_page.js',
230+
I: './data/I.js',
232231
},
233232
})
234233
expect(container.support('dummyPage')).is.ok

0 commit comments

Comments
 (0)