@starci/jest-preset 2.1.0 → 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 +21 -0
- package/README.md +5 -2
- package/index.cjs +18 -5
- package/index.d.ts +2 -0
- package/package.json +1 -1
- package/world-runner.cjs +69 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
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
|
+
|
|
20
|
+
## 2.1.1 - 2026-10-01
|
|
21
|
+
|
|
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.
|
|
23
|
+
|
|
3
24
|
## 2.1.0 - 2026-09-30
|
|
4
25
|
|
|
5
26
|
- Added: the claim side of `recordingOutbox()`, so a claim-side consumer is specced with the kit double the `OUTBOX` token requires (`spec-infra-double-from-kit`) instead of `mock<Outbox>()`. `queueRecords(...records)` fills a backlog that `claimDue(params)` hands out in order (only the asked queues, at most `limit`, removed once claimed; an empty backlog is an empty claim, a record queued twice is a duplicate delivery), `failNext(operation, error)` makes the next `enqueue`, `claimDue`, `complete`, `retry` or `bury` reject once, and `claims`, `completed`, `retried`, `buried` and `backlog` expose what happened. `RecordingOutbox<M, R>` takes the claimed record type as a second parameter; `ClaimedRecord`, `ClaimParams`, `RetryParams`, `BuryParams` and `OutboxOperation` are exported from `./outbox`. `clear()` resets the claim side too. No breaking change.
|
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`,
|
|
18
|
-
(`src/tests/world/global-setup.ts`, `global-teardown.ts`)
|
|
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,20 @@ 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
|
|
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
|
+
|
|
96
|
+
/** True when the repository at `root` has the test world's global setup (`src/tests/world/global-setup.ts`). */
|
|
97
|
+
function hasTestWorld(root) {
|
|
98
|
+
return require("node:fs").existsSync(require("node:path").join(root, "src", "tests", "world", "global-setup.ts"))
|
|
99
|
+
}
|
|
100
|
+
|
|
92
101
|
function starciJestConfig() {
|
|
93
102
|
const shared = {
|
|
94
103
|
preset: "ts-jest",
|
|
@@ -101,7 +110,9 @@ function starciJestConfig() {
|
|
|
101
110
|
...shared,
|
|
102
111
|
transform: transform("src/tests/tsconfig.json"),
|
|
103
112
|
displayName,
|
|
104
|
-
|
|
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,
|
|
105
116
|
testMatch: [`<rootDir>/src/tests/${displayName}/**/*.${suffix}.ts`],
|
|
106
117
|
// The one test world: started once per run of a project that has a test to run, torn down after it.
|
|
107
118
|
globalSetup: "<rootDir>/src/tests/world/global-setup.ts",
|
|
@@ -126,9 +137,10 @@ function starciJestConfig() {
|
|
|
126
137
|
testMatch: ["**/*.spec.ts"],
|
|
127
138
|
testPathIgnorePatterns: ["/node_modules/", ...TEST_KIND_FOLDERS.map(underTests)],
|
|
128
139
|
},
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
140
|
+
// The integration, e2e and contract projects exist only where the repository has its test world: jest validates
|
|
141
|
+
// every project's globalSetup even under --selectProjects unit, so a repository whose world is not written yet
|
|
142
|
+
// would otherwise fail to run its unit specs with the managed config.
|
|
143
|
+
...(hasTestWorld(process.cwd()) ? [suite("integration", "integration-spec"), suite("e2e", "e2e-spec"), suite("contract", "contract-spec")] : []),
|
|
132
144
|
],
|
|
133
145
|
}
|
|
134
146
|
}
|
|
@@ -154,4 +166,5 @@ module.exports = {
|
|
|
154
166
|
COVERAGE_EXCLUDES,
|
|
155
167
|
COVERAGE_THRESHOLD,
|
|
156
168
|
UNIT_COMPILER_OPTIONS,
|
|
169
|
+
WORLD_RUNNER,
|
|
157
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.
|
|
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,
|
package/world-runner.cjs
ADDED
|
@@ -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
|