Skip to content

Since 4.0: Scenario with a non-async one-line arrow gets no injected parameters (parse-function isMethod misdetection) #5679

Description

@mirao

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions