Skip to content

CodeceptJS 4.x: pause() doesn't stop at the right place — it pauses after subsequent steps instead of before them #5652

Closed

Description

@mirao

Tested with 4.0.x and 4.1 beta. Both are affected.
It works well with 3.7.9.

Run this test:

tests/My_test.ts:

Feature('My');

Scenario('test something', ({ I }) => {
    I.amOnPage('https://example.com');
    pause();
    I.see('Example Domain');
});

Expected result:

  • the pause() stops the test before I.see('Example Domain');

Actual result:

  • the pause() stops the test after I.see('Example Domain');
mirao@rog:~/workspace/my_esm_beta$ codeceptjs run --verbose
***************************************
nodeInfo:  24.16.0
osInfo:  Linux 6.17 Ubuntu 24.04.4 LTS 24.04.4 LTS (Noble Numbat)
cpuInfo:  (16) x64 AMD Ryzen 7 9700X 8-Core Processor
chromeInfo:  149.0.7827.200
edgeInfo:  "N/A"
firefoxInfo:  152.0.3
safariInfo:  N/A
playwrightBrowsers:  "chromium: 149.0.7827.55, firefox: 151.0, webkit: 26.5"
If you need more detailed info, just run this: npx codeceptjs info
***************************************
CodeceptJS v4.0.8 #StandWithUkraine
Using test root "/home/mirao/workspace/my_esm_beta"
Helpers: Playwright
Plugins: screenshot

My --
/home/mirao/workspace/my_esm_beta/tests/My_test.ts
    [1]  Starting recording promises
    Timeouts: 
 › [Session] Starting singleton browser session
  test something
 › [New Session] {"ignoreHTTPSErrors":false,"acceptDownloads":true}
  Scenario()
    I am on page "https://example.com"
    I see "Example Domain"
 Interactive shell started
 Use JavaScript syntax to try steps in action
 - Press ENTER to run the next step
 - Press TAB twice to see all available commands
 - Type exit + Enter to exit the interactive shell
 - Prefix => to run js commands 
 I.

Workaround:

  • Use await pause()

Configuration:

codecept.conf.ts

export const config: CodeceptJS.MainConfig = {
  tests: './tests/*_test.ts',
  output: './output',
  helpers: {
    Playwright: {
      browser: 'chromium',
      url: 'http://localhost',
      show: true
    }
  },
  include: {
    I: './steps_file.ts'
  },
  noGlobals: true,
  plugins: {},
  name: 'my_esm_beta',
  require: ['tsx/cjs']
}

package.json

{
  "devDependencies": {
    "@types/node": "^26.0.1",
    "codeceptjs": "^4.0.8",
    "playwright": "^1.61.1",
    "tsx": "^4.22.4",
    "typescript": "^6.0.3"
  }
}

Tested with NodeJS v24.16.0

Activity

  1. mirao commented on Jun 30, 2026

    @mirao
    ContributorAuthor

    Root cause & suggested fix

    The regression comes from the global pause being turned into an async function with a lazy dynamic import in lib/globals.js:

    global.pause = async (...args) => {
      const pauseModule = await import('./pause.js')
      return (pauseModule.default || pauseModule)(...args)
    }

    A scenario body runs synchronously to build the recorder queue — every I.* call does recorder.add(...) synchronously via recordStep. But because pause is now async, pause() returns at the await import(...) and its recorder.add('Start new session', ...) is deferred to a later microtask, after the rest of the synchronous body (the following steps) has already been queued.

    DEBUG=codeceptjs:recorder trace for the minimal repro shows it clearly:

    Queued | amOnPage
    Queued | see: "Example Domain"     <-- whole body queued synchronously
    Running | amOnPage
    Queued | Start new session         <-- pause inserted AFTER `see`
    

    So the pause lands at the tail of the queue and stops after the following steps. This also explains why await pause() works as a workaround: awaiting suspends the synchronous body until pause's recorder.add has run, so the next steps are queued after it.

    Suggested fix

    initCodeceptGlobals is already async, so resolve the module up front and assign the real function directly (synchronous at call time):

    const pauseModule = await import('./pause.js')
    global.pause = pauseModule.default || pauseModule

    With this, ordering is restored:

    Queued | amOnPage
    Queued | Start new session         <-- between the two steps
    Queued | see: "Example Domain"
    Running | amOnPage -> Start new session -> Interactive shell started   (before `see`)
    

    I've verified this fix locally via patch-package against codeceptjs@4.0.8 and pause() now stops in the right place without await.

    Note: global.within and global.session use the same async-lazy-import pattern, so they share the same latent hazard (normally masked because they're awaited).


    🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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