fix(effects): make within/tryTo/hopeThat/retryTo work under tsx/cjs (#5632)#5634
fix(effects): make within/tryTo/hopeThat/retryTo work under tsx/cjs (#5632)#5634mirao wants to merge 1 commit into
Conversation
…odeceptjs#5632) When a TypeScript test imports effects through a CommonJS loader (`import { tryTo } from 'codeceptjs/effects'` under `tsx/cjs`), Node loads a second, disconnected CJS copy of effects.js together with its own copies of the `recorder` and `container` singletons. That recorder is never started (`running=false`), so every `recorder.add()` short-circuits: tryTo() and hopeThat() return undefined without running their callback, within() silently skips its steps (and its empty container never calls `_withinBegin`), and retryTo() never resolves and hangs. This is the default situation for essentially every TS project, because CodeceptJS loads test files through Mocha's synchronous `require()` (CJS realm) while the framework itself runs as ESM. Fix by making the singletons realm-agnostic via the same globalThis bridge the framework already uses for `global.codeceptjs`: - recorder.js: start() registers the running instance on `globalThis.__codeceptjs_recorder` (only the started instance registers). - container.js: create() registers the live container on `globalThis.__codeceptjs_container`. - effects.js: resolve recorder/container through `_getRecorder()` / `_getContainer()`, which prefer the globalThis instances and fall back to the local imports under pure ESM (no behavior change there). Adds a runner regression test (test/data/effects-tsx-cjs + effects_tsx_test.js) that drives a real `tsx/cjs` project and asserts all four effects execute. It fails without the fix (and a timeout guard keeps a regressed retryTo() from hanging the suite) and passes with it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (codeceptjs#5635) codeceptjs#5635 fixes the dual-realm split at the source (shared recorder/container), which makes within/tryTo/hopeThat/retryTo work from a CJS-loaded test without the effects.js indirection PR codeceptjs#5634 added — i.e. codeceptjs#5635 supersedes codeceptjs#5634's mechanism. To avoid losing codeceptjs#5634's regression coverage, fold its effects assertions into this PR's fixture: extend the helper with the effects-exercising methods and add effects_test.ts, then assert the markers from the same single fixture run. retryTo is kept last and the child exec has a timeout guard so a future regression that re-hangs it fails cleanly instead of hanging. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Heads-up: #5636 (fix for #5635) addresses the same dual-realm split as this PR, but at the source rather than in #5636 makes the I verified empirically on the #5636 branch (which does not include this PR — So this PR's To avoid losing the valuable regression coverage from this PR, I've ported its effects test into #5636 ( Suggest we close this PR in favor of #5636 — but happy to keep it if you'd prefer the narrower change first. Either way the effects coverage is preserved. |
Fixes #5632
Problem
within()andtryTo()(andhopeThat()/retryTo()) silently do nothing in 4.x when a TypeScript test imports effects through a CommonJS loader, e.g.:run under
require: ['tsx/cjs'](the setup the official Quickstart generates).Root cause
CodeceptJS 4.x is native ESM, and it loads test files through Mocha's synchronous
require()(lib/mocha/factory.js), i.e. the CommonJS realm. So when a test importscodeceptjs/effects,tsx/cjsturns it into arequire()and Node materializes a second, disconnected CJS copy ofeffects.jstogether with its own copies of therecorderandcontainersingletons.That recorder is never started (
running = false), so everyrecorder.add()short-circuits toPromise.resolve():tryTo()/hopeThat()returnundefinedwithout ever running their callback (no error is reported even if the inner steps would fail),within()skips all of its inner steps, and its empty CJS container never calls_withinBegin, so the context is never applied,retryTo()never resolves and hangs.Because test files are always loaded via
require(), this is the default for essentially every TypeScript project — including the bare Quickstart.Fix
Make the singletons realm-agnostic using the same
globalThisbridge the framework already uses forglobal.codeceptjs(lib/globals.js):lib/recorder.js—start()registers the running instance onglobalThis.__codeceptjs_recorder(only the instance the runner actually starts registers itself).lib/container.js—create()registers the live container onglobalThis.__codeceptjs_container.lib/effects.js— resolves the recorder/container through_getRecorder()/_getContainer(), which prefer theglobalThisinstances and fall back to the local module imports. Under pure ESM both point at the same object, so there is no behavior change on the ESM path.Tests
Adds a runner regression test that drives a real
tsx/cjsproject and asserts all four effects execute:test/data/effects-tsx-cjs/— fixture (config withrequire: ["tsx/cjs"], a helper, and a test that imports effects through the CJS loader).test/runner/effects_tsx_test.js— assertswithin,tryTo,hopeThatandretryToall run.It fails without the fix and passes with it. The
execcall has a timeout guard so that a future regression which re-hangsretryTo()fails cleanly instead of hanging the suite.The existing pure-ESM coverage (
test/unit/effects_test.js,test/runner/within_test.js) continues to pass — that path previously had no coverage for the CJS-loaded case, which is why the regression shipped.🤖 Generated with Claude Code