You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
CodeceptJS 4.x: pause() doesn't stop at the right place — it pauses after subsequent steps instead of before them #5652
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 beforeI.see('Example Domain');
Actual result:
the pause() stops the test afterI.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.
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):
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).
Activity
mirao commentedon Jun 30, 2026
Root cause & suggested fix
The regression comes from the global
pausebeing turned into an async function with a lazy dynamic import inlib/globals.js:A scenario body runs synchronously to build the recorder queue — every
I.*call doesrecorder.add(...)synchronously viarecordStep. But becausepauseis now async,pause()returns at theawait import(...)and itsrecorder.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:recordertrace for the minimal repro shows it clearly: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'srecorder.addhas run, so the next steps are queued after it.Suggested fix
initCodeceptGlobalsis alreadyasync, so resolve the module up front and assign the real function directly (synchronous at call time):With this, ordering is restored:
I've verified this fix locally via
patch-packageagainstcodeceptjs@4.0.8andpause()now stops in the right place withoutawait.Note:
global.withinandglobal.sessionuse the same async-lazy-import pattern, so they share the same latent hazard (normally masked because they're awaited).🤖 Generated with Claude Code
fix(pause): assign global.pause synchronously so it stops at the righ…