From 6a940e333921a7367b2c6122e909bf6ace48b036 Mon Sep 17 00:00:00 2001 From: Jaromir Obr Date: Tue, 30 Jun 2026 09:46:55 +0200 Subject: [PATCH] fix(pause): assign global.pause synchronously so it stops at the right step (#5652) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- lib/globals.js | 6 ++---- test/unit/pause_test.js | 48 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/lib/globals.js b/lib/globals.js index 4ebdd4bf7..816cfc40a 100644 --- a/lib/globals.js +++ b/lib/globals.js @@ -30,10 +30,8 @@ export async function initCodeceptGlobals(dir, config, container) { // pause/inject/share stay global even under noGlobals — they're the everyday // debugging/wiring entry points and have no useful import alternative for // page-object code that runs before the container is available. - global.pause = async (...args) => { - const pauseModule = await import('./pause.js') - return (pauseModule.default || pauseModule)(...args) - } + const pauseModule = await import('./pause.js') + global.pause = pauseModule.default || pauseModule global.inject = () => container.support() global.share = container.share diff --git a/test/unit/pause_test.js b/test/unit/pause_test.js index 50a0b44db..7f91b079a 100644 --- a/test/unit/pause_test.js +++ b/test/unit/pause_test.js @@ -4,6 +4,7 @@ import pause, { setPauseHandler, pauseNow } from '../../lib/pause.js' import recorder from '../../lib/recorder.js' import event from '../../lib/event.js' import store from '../../lib/store.js' +import { initCodeceptGlobals } from '../../lib/globals.js' const settles = (promise, ms = 2000) => Promise.race([ @@ -100,3 +101,50 @@ describe('pause listener lifecycle', () => { expect(recorder.getCurrentSessionId()).to.equal(null) }) }) + +describe('global pause() queue ordering (issue #5652)', () => { + let savedPause + let savedNoGlobals + + beforeEach(async () => { + savedPause = global.pause + savedNoGlobals = store.noGlobals + store.dryRun = false + setPauseHandler(() => Promise.resolve()) + recorder.reset() + recorder.stop() + await initCodeceptGlobals(process.cwd(), { output: 'output', noGlobals: true }, { support() {}, share() {} }) + }) + + afterEach(() => { + setPauseHandler(null) + event.dispatcher.emit(event.test.finished) + recorder.reset() + global.pause = savedPause + store.noGlobals = savedNoGlobals + }) + + it('global.pause is a synchronous function, not an async wrapper', () => { + expect(global.pause).to.equal(pause) + expect(global.pause.constructor.name).to.equal('Function') + }) + + it('queues the pause step between the steps surrounding it in the test body', () => { + // Mirrors a scenario body that runs synchronously to build the recorder queue: + // I.amOnPage(...); pause(); I.see(...); + // The pause step must land *before* `see`, not after the rest of the body. + recorder.start() + recorder.add('amOnPage', () => {}) + global.pause() + recorder.add('see', () => {}) + + const order = recorder.scheduled().split('\n') + const amOnPageIdx = order.indexOf('amOnPage') + const pauseIdx = order.indexOf('Start new session') + const seeIdx = order.indexOf('see') + + expect(pauseIdx, 'pause step must be queued synchronously').to.be.greaterThan(-1) + expect(pauseIdx, 'pause must be queued after the preceding step').to.be.greaterThan(amOnPageIdx) + expect(pauseIdx, 'pause must be queued before the following step').to.be.lessThan(seeIdx) + }) +})