@starci/jest-preset 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.2.0 - 2026-10-01
4
+
5
+ - Fixed: in one `npm run test:e2e` every e2e file after the first failed to boot with "Cannot determine a GraphQL output type":
6
+ `@nestjs/graphql` keeps its type registry on the process global, the test world's globalSetup loads application code in jest's
7
+ main process, and jest-environment-node exposes the host process's globals to every file it runs there (or in a reused worker).
8
+ The integration, e2e and contract projects now run on the preset's world runner (`runner: WORLD_RUNNER`, `world-runner.cjs`):
9
+ every spec file in a fresh worker process, never in band (not even with `--runInBand` or one
10
+ test), so each file boots clean whatever framework keeps state on a global. The runner drives the stock jest-runner of the jest
11
+ that loads it (jest -> @jest/core -> jest-runner), one single-worker farm per file.
12
+ - Changed: the world runner runs the files of the integration, e2e and contract projects ONE AT A TIME, enforced by the runner
13
+ (`--maxWorkers` is not honoured there): every file shares the run's data and the test world resets it when a file boots, so
14
+ two files at once would wipe each other's state. The project-level `maxWorkers: 1` is removed: it is a global jest option a
15
+ project config never applied (so e2e files used to run cores-1 at a time).
16
+ - Known limitation (lifted in alpha.5): world files cannot run in parallel until each worker has its own data namespace (a
17
+ database schema, a Keycloak realm prefix, a Redis key prefix and its own fakes per worker).
18
+ - Added: `WORLD_RUNNER` (the runner's resolved path) on the package root.
19
+
3
20
  ## 2.1.1 - 2026-10-01
4
21
 
5
22
  - Fixed: a repository without its test world (`src/tests/world/global-setup.ts`) could not run its unit specs with the managed `jest.config.js`: jest validates every project's `globalSetup`, even under `--selectProjects unit`. The integration, e2e and contract projects are now declared only when the world exists.
package/README.md CHANGED
@@ -14,8 +14,11 @@ module.exports = require("@starci/jest-preset").starciJestConfig()
14
14
  - **Four projects, one per test kind**: `unit` (`**/*.spec.ts`, which by the unit standard means one `<name>.service.spec.ts` beside each `*.service.ts`; `clearMocks`; the Outcome matchers are installed through `setupFilesAfterEnv`; ignores
15
15
  `src/tests/{world,integration,e2e,contract}/`), `integration` (`src/tests/integration/**/*.integration-spec.ts`), `e2e`
16
16
  (`src/tests/e2e/**/*.e2e-spec.ts`) and `contract` (`src/tests/contract/**/*.contract-spec.ts`). The last three compile against
17
- `src/tests/tsconfig.json`, run one worker, and start the one test world through `globalSetup`/`globalTeardown`
18
- (`src/tests/world/global-setup.ts`, `global-teardown.ts`); only the unit project has a `setupFilesAfterEnv`. The managed scripts select one
17
+ `src/tests/tsconfig.json`, start the one test world through `globalSetup`/`globalTeardown`
18
+ (`src/tests/world/global-setup.ts`, `global-teardown.ts`) and run on the world runner (`world-runner.cjs`, `WORLD_RUNNER`): every
19
+ spec file in a worker process of its own and never in band, so nothing the globalSetup or another file left on a process global
20
+ (a framework registry such as `@nestjs/graphql`'s type metadata) reaches the file; and ONE FILE AT A TIME whatever
21
+ `--maxWorkers` says, because every file shares the run's data (the test world resets it when a file boots). Only the unit project has a `setupFilesAfterEnv`. The managed scripts select one
19
22
  project each: `test`, `test:integration`, `test:e2e`, `test:contract`; a contract spec skips itself without sandbox config.
20
23
  - **The three path aliases** `@features/*`, `@modules/*`, `@tests/*` (`MODULE_NAME_MAPPER`), the same ones the managed `tsconfig.json` declares.
21
24
  - **ts-jest with `diagnostics: false`** in every project. `isolatedModules: true` comes from `@starci/tsconfig` (ts-jest 29.4 reads it
package/index.cjs CHANGED
@@ -84,11 +84,15 @@ const underTests = (folder) => String.raw`[\\/]src[\\/]tests[\\/]` + folder + St
84
84
  * contract project is never part of `test` or `test:e2e`, and a contract spec skips itself without sandbox config. The
85
85
  * test tree compiles against `src/tests/tsconfig.json`, the nearest config of every file under `src/tests/`. The
86
86
  * integration, e2e and contract projects share the world's `global-setup.ts`/`global-teardown.ts` (`src/tests/world/`) and
87
- * run one worker; the unit project alone has a `setupFilesAfterEnv` (the Outcome matchers).
87
+ * run their spec files one at a time, each in a worker process of its own (`world-runner.cjs`; every file shares the
88
+ * run's data, so the runner never runs two at once); the unit project alone has a `setupFilesAfterEnv` (the Outcome matchers).
88
89
  * Coverage is collected from every `*.service.ts` only, with a per-file threshold of 100 on lines, branches, functions and
89
90
  * statements: the `test` script runs the unit project with `--coverage` and fails below it. It uses v8: istanbul instruments the helpers TypeScript emits (`__decorate`, `__param`, `__awaiter`, interop wrappers)
90
91
  * as thousands of branches no spec can cover, while v8 measures the real source.
91
92
  */
93
+ /** The runner of the integration, e2e and contract projects: one fresh worker process per spec file. */
94
+ const WORLD_RUNNER = require.resolve("./world-runner.cjs")
95
+
92
96
  /** True when the repository at `root` has the test world's global setup (`src/tests/world/global-setup.ts`). */
93
97
  function hasTestWorld(root) {
94
98
  return require("node:fs").existsSync(require("node:path").join(root, "src", "tests", "world", "global-setup.ts"))
@@ -106,7 +110,9 @@ function starciJestConfig() {
106
110
  ...shared,
107
111
  transform: transform("src/tests/tsconfig.json"),
108
112
  displayName,
109
- maxWorkers: 1,
113
+ // Every spec file in a process of its own (never in band): nothing one file or the globalSetup put on a process global
114
+ // (a framework registry such as @nestjs/graphql's type metadata) reaches another file. See world-runner.cjs.
115
+ runner: WORLD_RUNNER,
110
116
  testMatch: [`<rootDir>/src/tests/${displayName}/**/*.${suffix}.ts`],
111
117
  // The one test world: started once per run of a project that has a test to run, torn down after it.
112
118
  globalSetup: "<rootDir>/src/tests/world/global-setup.ts",
@@ -160,4 +166,5 @@ module.exports = {
160
166
  COVERAGE_EXCLUDES,
161
167
  COVERAGE_THRESHOLD,
162
168
  UNIT_COMPILER_OPTIONS,
169
+ WORLD_RUNNER,
163
170
  }
package/index.d.ts CHANGED
@@ -19,4 +19,6 @@ export declare const COVERAGE_SOURCES: string[]
19
19
  export declare const COVERAGE_EXCLUDES: string[]
20
20
  /** The compiler options the unit project overlays on the repository tsconfig so per-file 100 coverage of a decorated service is reachable. */
21
21
  export declare const UNIT_COMPILER_OPTIONS: Readonly<{ isolatedModules: false; importHelpers: true }>
22
+ /** The runner of the integration, e2e and contract projects (`world-runner.cjs`): every spec file in a worker process of its own. */
23
+ export declare const WORLD_RUNNER: string
22
24
  export declare const COVERAGE_THRESHOLD: Readonly<{ lines: 100; branches: 100; functions: 100; statements: 100 }>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starci/jest-preset",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "The StarCi jest config for Nest repositories (unit, integration, e2e and contract projects, ts-jest with diagnostics off, per-file 100 coverage of *.service.ts only) and the one shared unit-test double kit: typed mock<T>(), mockEntityManager(), fakeTransaction(), fakeCache(), fakeLock(), recordingOutbox(), FakeClock, fakeIds(), builder() and the Outcome matchers toBeRefused/toSucceedWith.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -0,0 +1,69 @@
1
+ "use strict"
2
+
3
+ /**
4
+ * The runner of the integration, e2e and contract projects: every spec file runs in a process of its own.
5
+ *
6
+ * Why: jest runs a file in a fresh vm context, but a context still reaches the globals of the process that hosts it, and
7
+ * jest-environment-node exposes every global the host process had when the environment loaded. The test world's globalSetup
8
+ * runs in jest's main process and loads application code there (the migrate step and its modules), so a framework that keeps
9
+ * its registry on a process global (`@nestjs/graphql` keeps `GqlTypeMetadataStorage` on `global`) leaks it into every file
10
+ * jest runs in that process; and a worker that runs several files carries whatever the first one left on its own global. In
11
+ * one `npm run test:e2e` the second file then fails to boot with "Cannot determine a GraphQL output type". Resetting the
12
+ * framework's storage would chase one framework at a time; a process per file isolates every one of them.
13
+ *
14
+ * How: the stock jest-runner, driven one file at a time with a single-worker farm (`maxWorkers: 1` per file), which forks a
15
+ * fresh child, runs the file there and ends the child. Never in band, even when jest would choose it (one worker, one test,
16
+ * `--runInBand`): the main process holds the globalSetup's state.
17
+ *
18
+ * ONE FILE AT A TIME, whatever `--maxWorkers` says: every file of a run shares the run's data (the test world resets the run's
19
+ * databases, realm users, Redis DB and fakes when a file boots), so a second file running beside the first would wipe what the
20
+ * first is using. The runner enforces it, so no flag or script can turn it off. Per-worker data namespaces are the known
21
+ * limitation that lifts it (see CHANGELOG 2.2.0).
22
+ */
23
+
24
+ const { createRequire } = require("node:module")
25
+ const path = require("node:path")
26
+
27
+ /**
28
+ * The stock jest-runner of the jest that loads this preset: resolved through jest's own dependency chain
29
+ * (jest -> @jest/core -> jest-runner) from the repository root, so the runner always matches the running jest.
30
+ */
31
+ function stockRunner(root = process.cwd()) {
32
+ const fromRoot = createRequire(path.join(root, "package.json"))
33
+ const fromJest = createRequire(fromRoot.resolve("jest/package.json"))
34
+ const fromCore = createRequire(fromJest.resolve("@jest/core"))
35
+ const loaded = fromCore("jest-runner")
36
+ return loaded.default ?? loaded
37
+ }
38
+
39
+ const StockRunner = stockRunner()
40
+
41
+ class WorldRunner extends StockRunner {
42
+ #listeners = []
43
+
44
+ /** Jest subscribes before the run; the subscriptions are handed to the per-file runner of each file. */
45
+ on(eventName, listener) {
46
+ const entry = { eventName, listener }
47
+ this.#listeners.push(entry)
48
+ return () => {
49
+ this.#listeners = this.#listeners.filter((candidate) => candidate !== entry)
50
+ }
51
+ }
52
+
53
+ /** Runs the files one after another, each in a fresh worker process; `--maxWorkers` and `serial` (in band) are never honoured. */
54
+ async runTests(tests, watcher) {
55
+ const perFile = { ...this._globalConfig, maxWorkers: 1 }
56
+ for (const test of tests) {
57
+ if (watcher.isInterrupted()) return
58
+ const runner = new StockRunner(perFile, this._context)
59
+ const unsubscribe = this.#listeners.map(({ eventName, listener }) => runner.on(eventName, listener))
60
+ try {
61
+ await runner.runTests([test], watcher, { serial: false })
62
+ } finally {
63
+ for (const stop of unsubscribe) stop()
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ module.exports = WorldRunner