Skip to content

fix(effects): make within/tryTo/hopeThat/retryTo work under tsx/cjs (#5632)#5634

Closed
mirao wants to merge 1 commit into
codeceptjs:4.xfrom
mirao:fix/5632-effects-tsx-cjs
Closed

fix(effects): make within/tryTo/hopeThat/retryTo work under tsx/cjs (#5632)#5634
mirao wants to merge 1 commit into
codeceptjs:4.xfrom
mirao:fix/5632-effects-tsx-cjs

Conversation

@mirao

@mirao mirao commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Fixes #5632

Problem

within() and tryTo() (and hopeThat() / retryTo()) silently do nothing in 4.x when a TypeScript test imports effects through a CommonJS loader, e.g.:

import { within } from "codeceptjs";
import { tryTo } from "codeceptjs/effects";

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 imports codeceptjs/effects, tsx/cjs turns it into a require() and Node materializes 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 to Promise.resolve():

  • tryTo() / hopeThat() return undefined without 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 globalThis bridge the framework already uses for global.codeceptjs (lib/globals.js):

  • lib/recorder.jsstart() registers the running instance on globalThis.__codeceptjs_recorder (only the instance the runner actually starts registers itself).
  • lib/container.jscreate() registers the live container on globalThis.__codeceptjs_container.
  • lib/effects.js — resolves the recorder/container through _getRecorder() / _getContainer(), which prefer the globalThis instances 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/cjs project and asserts all four effects execute:

  • test/data/effects-tsx-cjs/ — fixture (config with require: ["tsx/cjs"], a helper, and a test that imports effects through the CJS loader).
  • test/runner/effects_tsx_test.js — asserts within, tryTo, hopeThat and retryTo all run.

It fails without the fix and passes with it. The exec call has a timeout guard so that a future regression which re-hangs retryTo() 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

…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>
mirao added a commit to mirao/CodeceptJS that referenced this pull request Jun 14, 2026
… (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>
@mirao

mirao commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

Heads-up: #5636 (fix for #5635) addresses the same dual-realm split as this PR, but at the source rather than in effects.js.

#5636 makes the recorder and container singletons themselves realm-agnostic (via a shared globalThis instance in the new lib/realm.js), so the CJS-loaded copy of effects.js resolves the live recorder/container directly through its normal import — no _getRecorder()/_getContainer() indirection needed. The same source-level fix also covers config, event, store, output, and transitively within/session/pause/inject/etc.

I verified empirically on the #5636 branch (which does not include this PR — effects.js is unpatched there) that all four effects run from a CJS-loaded test:

EFFECTS_TRYTO result=false
EFFECTS_CLICK withinActive=true
EFFECTS_PASS ran
EFFECTS_HOPETHAT result=true
EFFECTS_FLAKY try=2
EFFECTS_RETRY done   ← retryTo resolves, does not hang

So this PR's _getRecorder()/_getContainer() changes become redundant once #5636 lands (both sides of globalThis.__codeceptjs_recorder || recorder resolve to the same shared object).

To avoid losing the valuable regression coverage from this PR, I've ported its effects test into #5636 (test/data/internal-api-tsx-cjs/effects_test.ts + the corresponding runner assertions), so within/tryTo/hopeThat/retryTo under tsx/cjs stay guarded.

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.

@mirao

mirao commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

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.

Closing as it was fixed in #5636 by more general way.

@mirao mirao closed this Jun 14, 2026
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.

4.x: within() and tryTo() don't execute

1 participant