Introduced in 4.0. Our TypeScript suite ran fine on 3.7.x and started failing after the upgrade to 4.0. Reproduced on 4.0.9 and 4.1.0.
What are you trying to achieve?
Use a non-async arrow function with injected (destructured) parameters whose body is on a single line, e.g. Scenario('...', ({ I }) => { ... }) or Data(rows).Scenario('...', ({ current }) => { ... }).
TypeScript projects hit this on ordinary multi-line code, because tsx / esbuild collapse the transpiled function to one line and getParams() parses fn.toString().
What do you get instead?
Nothing is injected — I, page objects and current are all undefined. getParams() in lib/parser.js throws a SyntaxError, logs it and returns undefined, so the test then fails with TypeError: Cannot read properties of undefined.
Error in ({ I }) => { if (true) { I.say('hello') } }
SyntaxError: Unexpected token (1:3)
[1] Error | TypeError: Cannot read properties of undefined (reading 'say') undefined...
✖ BROKEN: I is not injected in 2ms
✔ OK: same code, multi-line in 2ms
✔ OK: same code, async in 0ms
Reproduction — no helper, no browser, no TypeScript needed:
mkdir cjs-parse-repro && cd cjs-parse-repro
npm init -y && npm pkg set type=module && npm i codeceptjs@4.1.0
npx codeceptjs run --verbose
// repro_test.js - in plain JavaScript the failing arrows must stay on one line
Feature('non-async one-line arrow loses injected params')
// prettier-ignore
Scenario('BROKEN: I is not injected', ({ I }) => { if (true) { I.say('hello') } })
Scenario('OK: same code, multi-line', ({ I }) => {
if (true) {
I.say('hello')
}
})
// prettier-ignore
Scenario('OK: same code, async', async ({ I }) => { if (true) { I.say('hello') } })
Cause
parse-function@5.6.10, dist/esm/index.js:322:
const isMethod = /^\*?.+\([\S\W]*\)\s*{/i.test(result.value);
if (!(isFunction || isAsyncFn || isAsyncArrow) && isMethod) {
// wraps the source in `{ }` so acorn can parse it as an object method
}
.+ is greedy and [\S\W]* matches everything, so the regex matches any source containing a ) { sequence — an if, switch, for or while in the arrow body is enough. The arrow is wrapped in braces as if it were an object method, and acorn throws. Async arrows escape via isAsyncArrow. In plain JavaScript a multi-line arrow also escapes, because . does not match a newline so the greedy .+ cannot reach past the first line.
That second escape does not exist under TypeScript: tsx / esbuild emit every function on one line, so fn.toString() returns the one-line form no matter how the source is written. In a TypeScript suite every non-async scenario with destructured parameters and a conditional in its body fails.
Prepending async to a non-async arrow before parser.parse() avoids the misdetection — that is what we currently patch into lib/parser.js locally.
I mentioned this earlier in #5641 (comment) but never filed it separately.
Details
- CodeceptJS version: 4.1.0 and 4.0.9 (not present in 3.7.x)
- NodeJS Version: 24.18.0
- Operating System: Ubuntu 24.04.4 LTS
- puppeteer || webdriverio || playwright version (if related): not related — reproduces with no helper configured
- Configuration file:
export const config = {
tests: './*_test.js',
output: './output',
helpers: {},
name: 'repro',
}
🤖 Investigated and drafted with Claude Code
Introduced in 4.0. Our TypeScript suite ran fine on 3.7.x and started failing after the upgrade to 4.0. Reproduced on 4.0.9 and 4.1.0.
What are you trying to achieve?
Use a non-async arrow function with injected (destructured) parameters whose body is on a single line, e.g.
Scenario('...', ({ I }) => { ... })orData(rows).Scenario('...', ({ current }) => { ... }).TypeScript projects hit this on ordinary multi-line code, because
tsx/esbuildcollapse the transpiled function to one line andgetParams()parsesfn.toString().What do you get instead?
Nothing is injected —
I, page objects andcurrentare allundefined.getParams()inlib/parser.jsthrows aSyntaxError, logs it and returnsundefined, so the test then fails withTypeError: Cannot read properties of undefined.Reproduction — no helper, no browser, no TypeScript needed:
Cause
parse-function@5.6.10,dist/esm/index.js:322:.+is greedy and[\S\W]*matches everything, so the regex matches any source containing a) {sequence — anif,switch,fororwhilein the arrow body is enough. The arrow is wrapped in braces as if it were an object method, and acorn throws. Async arrows escape viaisAsyncArrow. In plain JavaScript a multi-line arrow also escapes, because.does not match a newline so the greedy.+cannot reach past the first line.That second escape does not exist under TypeScript:
tsx/esbuildemit every function on one line, sofn.toString()returns the one-line form no matter how the source is written. In a TypeScript suite every non-async scenario with destructured parameters and a conditional in its body fails.Prepending
asyncto a non-async arrow beforeparser.parse()avoids the misdetection — that is what we currently patch intolib/parser.jslocally.I mentioned this earlier in #5641 (comment) but never filed it separately.
Details
🤖 Investigated and drafted with Claude Code