Skip to content

fix(pause): stop pause() at the right step in 4.x (#5652)#5653

Open
mirao wants to merge 1 commit into
codeceptjs:4.xfrom
mirao:fix/5652-pause-sync-ordering
Open

fix(pause): stop pause() at the right step in 4.x (#5652)#5653
mirao wants to merge 1 commit into
codeceptjs:4.xfrom
mirao:fix/5652-pause-sync-ordering

Conversation

@mirao

@mirao mirao commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Fixes #5652

Problem

In 4.x, pause() stops after the following step instead of before it:

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

Works correctly in 3.7.9; broken in 4.0.x and 4.1 beta.

Root cause

global.pause was wired as an async wrapper doing 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. Because pause was async, calling it returned at the await import(...), deferring its recorder.add('Start new session', ...) to a later microtask — after the rest of the synchronous body had already been queued.

DEBUG=codeceptjs:recorder for the repro:

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

This also explains why await pause() worked as a workaround: awaiting suspends the synchronous body until pause's recorder.add has run.

Fix

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

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

Ordering is restored:

Queued | amOnPage
Queued | Start new session         <-- between the two steps
Queued | see: "Example Domain"

Test

Added a regression test in test/unit/pause_test.js that wires global.pause via initCodeceptGlobals and asserts:

  • global.pause is the real (synchronous) pause function, not an async wrapper;
  • mirroring the repro (amOnPage → pause → see), the Start new session step is queued between the surrounding steps (checked synchronously via recorder.scheduled()).

The test fails against the old async-wrapper implementation and passes with the fix. Full test/unit suite is green (759 passing).

Note: global.within and global.session use the same async-lazy-import pattern and share the same latent hazard (currently masked because they're always awaited). Left out of scope here.

🤖 Generated with Claude Code

…t step (codeceptjs#5652)

global.pause was an async wrapper doing a lazy `await import('./pause.js')`.
A scenario body runs synchronously to build the recorder queue, so the await
deferred pause's `recorder.add('Start new session')` to a later microtask —
after the following steps had already been queued. As a result pause() stopped
*after* the next step instead of before it.

initCodeceptGlobals is already async, so resolve the module up front and assign
the real pause function directly, making it synchronous at call time and
restoring queue order.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant