fix(core): make the whole internal API work from tests/page objects under tsx/cjs (#5635)#5636
fix(core): make the whole internal API work from tests/page objects under tsx/cjs (#5635)#5636mirao wants to merge 4 commits into
Conversation
…/cjs (codeceptjs#5635) The internal API `config` imported via `import { config } from "codeceptjs"` returned an empty object `{}` from tests, page objects and fragments, while it worked from helpers. Test files are loaded through Mocha's synchronous require() (the CommonJS realm) under a CJS loader such as tsx/cjs, while the framework runs as native ESM. So a test importing `config` gets a second, disconnected CJS copy of config.js whose module-scoped `config` is an empty `{}` that the runner never populates. Helpers don't hit this because they are loaded via import() (the ESM realm) and share the live singleton. Bridge the live config through globalThis, the same way recorder.js and container.js were fixed in codeceptjs#5634: setConfig() mirrors the config onto globalThis.__codeceptjs_config on every create/append/reset, and Config.get() prefers it, falling back to the local module copy. Under pure ESM both point at the same object, so there is no behavior change on the ESM path. Adds a runner regression test driving a real tsx/cjs project that asserts a test (CJS realm) and a helper (ESM realm) both read the live config. It fails without the fix and passes with it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…odeceptjs#5635) Generalizes the config fix to every stateful internal-API singleton. The internal API (config, container, recorder, event, store, output) is documented to work from tests, page objects and fragments — not only helpers — but test files are loaded through Mocha's synchronous require() (the CommonJS realm) under a CJS loader such as tsx/cjs, while the framework runs as native ESM. So importing any of these from a test loaded a second, disconnected CJS copy: config.get() returned {}, the container had no helpers, the recorder was never started, the event dispatcher had no listeners. Helpers were unaffected because they load via import() (the ESM realm). Add lib/realm.js with realmSingleton(key, factory), which stores each singleton on globalThis so every realm resolves to the one the runner operates on (the ESM runner loads these modules first during bootstrap, so it wins the key; later CJS copies reuse it). Apply it to recorder, container state, the event dispatcher, store and output, and refactor config.js to the same holder pattern. Under pure ESM the modules load once, so there is no behavior change. Broadens the regression fixture to assert config/container/recorder/event/store all resolve to the live singletons from a test imported via tsx/cjs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nt (codeceptjs#5635) The previous commit wrapped the output/store/container singletons by inlining their object literal into the realmSingleton() factory call. tsd-jsdoc (npm run def) couldn't associate the `@namespace`/`@type` JSDoc with a literal nested in an arrow argument, so the regenerated typings dropped the `output` and `store` namespaces and lost the Result reference — breaking dtslint in CI. Keep the plain object literal (so jsdoc still sees it) and share it at export time via realmSingleton(() => literal), the same shape recorder.js already uses. Runtime behavior is unchanged; `npm run def` now regenerates the committed typings unchanged and dtslint passes. 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>
|
what is realm? 🙄 |
DavertMik
left a comment
There was a problem hiding this comment.
I think Claude took the wrong path here
First: what the actual problem is? What we are trying to achieve?
Which exact internal API should work from tests?
I don't think that most of internal APIs should work from tests, as tests are business logic driven, and internal APIs are implementation details. I'd rather not to explose implementatoin details into tests / page objetcs themselves and keeped correct level of abstractoin
In 3.x I used After the migration to 4.x it stopped working 😞 . Also the stuff like stopped working (#5632) in 4.x Therefore I needed some solution. |
|
Thanks! I will try to ask my claude about this |
|
OK, thanks. Maybe https://codecept.io/architecture#the-internal-api should be then updated so that the scope of internal API is more clear for users (where and which methods can be used). As for the issue with the effects such as |
Fixes #5635
Problem
The internal API (https://codecept.io/architecture#the-internal-api) is supposed to work from tests, page objects and fragments — not only helpers. In 4.x it works only from helpers:
configwas just the reported symptom —container,recorder,eventandstoreare affected the same way. Works fine in 3.x.Root cause
Same dual-realm split as #5634, but for the public singletons.
CodeceptJS 4.x is native ESM, but Mocha loads test files through a synchronous
require()(the CommonJS realm). Under a CJS loader such astsx/cjs(the setup the official Quickstart generates), a test that imports the internal API gets a second, disconnected CJS copy of eachlibmodule and its singleton — a copy the runner never populates:config.get()returns{},containerhas no helpers/support/plugins,recorderis never started (so effects short-circuit),eventdispatcher has no listeners.Helpers don't hit this because they're loaded via
import()(lib/container.js), i.e. the ESM realm, so they share the live singletons — which is why the bug looked like "helpers only".Fix
Share each stateful singleton across realms through
globalThis, the same mechanism used for the recorder/container bridges in #5634, generalized:lib/realm.js(new) —realmSingleton(key, factory)stores the instance onglobalThisso every realm resolves to the one the runner operates on. The ESM runner loads these modules first during bootstrap, so it wins thekey; later CJS copies reuse it.lib/recorder.js(recorder object),lib/container.js(state object),lib/event.js(dispatcher),lib/store.jsandlib/output.js.lib/config.jsrefactored to the same holder pattern.Under pure ESM the modules load once, so there is no behavior change on the ESM path.
Tests
Adds a runner regression test driving a real
tsx/cjsproject:test/data/internal-api-tsx-cjs/— fixture (config withrequire: ["tsx/cjs"], a helper, and a test that imports the internal API through the CJS loader).test/runner/internal_api_tsx_test.js— assertsconfig,container,recorder,eventandstoreall resolve to the live singletons from a test, and that a helper (ESM realm) sees the same config.It fails without the fix and passes with it. The full unit suite (747 passing) and runner suite continue to pass.
🤖 Generated with Claude Code