@starci/jest-preset 1.1.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/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # @starci/jest-preset
2
+
3
+ The jest config of a Nest repository, and the typed `mock<T>()` helper. Install it from the npm registry at the exact version in [`knowledge/hfs/canon-pins.yaml`](../../knowledge/hfs/canon-pins.yaml) (see [`packages/README.md`](../README.md)).
4
+
5
+ ```js
6
+ // jest.config.js
7
+ const { pathsToModuleNameMapper } = require("ts-jest")
8
+ const { starciJestConfig } = require("@starci/jest-preset")
9
+ const { compilerOptions } = require("./tsconfig.json")
10
+
11
+ module.exports = starciJestConfig({
12
+ moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: `${__dirname}/` }),
13
+ e2e: { globalSetup: "<rootDir>/src/tests/e2e/setup/global-setup.js" },
14
+ })
15
+ ```
16
+
17
+ ## What it sets
18
+
19
+ - **Two projects**: `unit` (`**/*.spec.ts`, mocked, `clearMocks`) and `e2e` (`src/tests/e2e/**/*.e2e-spec.ts`, one worker, 120 s).
20
+ `src/tests/e2e/live/` is skipped unless `E2E_LIVE=1`; `test:e2e:live` is `E2E_LIVE=1 jest --selectProjects e2e src/tests/e2e/live`.
21
+ - **ts-jest with `diagnostics: false`** in both. `isolatedModules: true` comes from `@starci/tsconfig` (ts-jest 29.4 reads it
22
+ there; its own option is deprecated). Types are checked once, by `typecheck` (unit specs included) and `typecheck:e2e`, and
23
+ `test:e2e` is `npm run typecheck:e2e && jest --selectProjects e2e`, so nobody runs e2e on code that does not type-check.
24
+ - **Coverage that matches Sonar.** `collectCoverageFrom` is `src/**/*.ts` and `apps/**/*.ts` minus specs, e2e specs, `dist`,
25
+ `coverage`, `src/tests/**`, `*.d.ts` and `main.ts`: the same set Sonar counts. `sonarExclusions()` and
26
+ `sonarCoverageExclusions()` render the two `sonar-project.properties` values from the same lists, so the denominators
27
+ cannot drift. Reports are `coverage/lcov.info` (Codecov and Sonar read the one file) and a text summary. Thresholds are not
28
+ set here; Codecov's project and patch targets own the gate.
29
+
30
+ ## `mock<T>()`
31
+
32
+ Replaces `{ ... } as unknown as T`.
33
+
34
+ ```ts
35
+ import { mock } from "@starci/jest-preset"
36
+
37
+ const repo = mock<UserRepository>({ name: "users" }) // overrides may be partial
38
+ repo.find.mockResolvedValue({ id: "a" }) // typed against UserRepository["find"]
39
+ const service = new UserService(repo) // assignable wherever a UserRepository is required
40
+ expect(repo.find).toHaveBeenCalledWith("a")
41
+ ```
42
+
43
+ Every property read yields one stable `jest.fn()`, created on first read. The double is not thenable, so returning it from
44
+ an async function does not hang; `then`, `toJSON` and Symbol keys read as absent. A value you pass in `overrides` wins. Wrong
45
+ stubs (`repo.find.mockResolvedValue(1)`) and members the type does not have are compile errors. Fixtures use it instead of
46
+ `as never` and `as unknown as`.
47
+
48
+ ## `FakeClock`
49
+
50
+ The test double for the injected `Clock` port (`platform/clock`). Business code asks `clock.now()` instead of reading
51
+ `Date.now()` or `new Date()` ambiently (`no-ambient-clock`, `@starci/eslint-canon-be` R79 `BE_AMBIENT_CLOCK`); a spec
52
+ drives time explicitly instead of sleeping for real or racing the wall clock.
53
+
54
+ ```ts
55
+ import { FakeClock } from "@starci/jest-preset/clock"
56
+
57
+ const clock = new FakeClock("2026-01-01T00:00:00.000Z") // starts at a chosen instant; no argument starts at real now
58
+ const service = new PlanService(clock)
59
+
60
+ clock.advance(60_000) // move the clock forward (or back, with a negative value) without a real sleep
61
+ clock.set("2026-06-15T00:00:00.000Z") // jump to a chosen instant, e.g. to drive a scheduled job
62
+ expect(clock.now().toISOString()).toBe("2026-06-15T00:00:00.000Z")
63
+ ```
64
+
65
+ `FakeClock` satisfies the `Clock` port structurally (`{ now(): Date }`); no cast is needed to inject it in place of the
66
+ real clock.
package/clock.cjs ADDED
@@ -0,0 +1,34 @@
1
+ "use strict"
2
+
3
+ /**
4
+ * `FakeClock` -- the test double for the injected `Clock` port (`platform/clock`).
5
+ *
6
+ * Business code asks its `Clock` port for the time (`clock.now()`) instead of reading `Date.now()` or
7
+ * `new Date()` ambiently (`no-ambient-clock`, R79). A spec then drives time explicitly: it starts the
8
+ * clock at a chosen instant, advances it by a duration, or sets it to a new instant, all without a real
9
+ * sleep and without a race against the wall clock.
10
+ */
11
+
12
+ class FakeClock {
13
+ /** @param {Date | number | string} [at] - The instant the clock starts at; defaults to now, once, at construction. */
14
+ constructor(at) {
15
+ this._now = at === undefined ? new Date() : new Date(at)
16
+ }
17
+
18
+ /** The current instant, matching the `Clock` port's `now(): Date`. */
19
+ now() {
20
+ return new Date(this._now.getTime())
21
+ }
22
+
23
+ /** Moves the clock to `at`. */
24
+ set(at) {
25
+ this._now = new Date(at)
26
+ }
27
+
28
+ /** Moves the clock forward by `ms` milliseconds (negative moves it back). */
29
+ advance(ms) {
30
+ this._now = new Date(this._now.getTime() + ms)
31
+ }
32
+ }
33
+
34
+ module.exports = { FakeClock }
package/clock.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /** The test double for the injected `Clock` port: `{ now(): Date }`, plus spec-only time control. */
2
+ export declare class FakeClock {
3
+ constructor(at?: Date | number | string)
4
+
5
+ /** The current instant. Matches the `Clock` port's `now(): Date`, so `FakeClock` satisfies it structurally. */
6
+ now(): Date
7
+
8
+ /** Moves the clock to `at`. */
9
+ set(at: Date | number | string): void
10
+
11
+ /** Moves the clock forward by `ms` milliseconds (negative moves it back). */
12
+ advance(ms: number): void
13
+ }
package/index.cjs ADDED
@@ -0,0 +1,100 @@
1
+ "use strict"
2
+
3
+ const { mock, createMock } = require("./mock.cjs")
4
+
5
+ /**
6
+ * What Sonar counts as production code, expressed once. `sonarCoverageExclusions` renders the same list for
7
+ * `sonar.coverage.exclusions`, so the jest denominator and the Sonar denominator cannot drift.
8
+ *
9
+ * Sonar side (canon sonar-project.properties): sources = src,apps; exclusions = specs, e2e specs, dist, coverage;
10
+ * coverage.exclusions = src/tests/**, e2e specs, entrypoints and declaration files.
11
+ */
12
+ const COVERAGE_SOURCES = ["src/**/*.ts", "apps/**/*.ts"]
13
+ /** Sonar `sonar.exclusions`: not analysed at all, so neither counted nor covered. */
14
+ const SONAR_EXCLUSIONS = ["**/*.spec.ts", "**/*.e2e-spec.ts", "**/dist/**", "**/coverage/**"]
15
+ /** Sonar `sonar.coverage.exclusions`: analysed, but outside the coverage denominator. */
16
+ const COVERAGE_ONLY_EXCLUSIONS = ["src/tests/**", "**/*.d.ts", "**/main.ts"]
17
+ const COVERAGE_EXCLUDES = [...SONAR_EXCLUSIONS, ...COVERAGE_ONLY_EXCLUSIONS]
18
+
19
+ function collectCoverageFrom() {
20
+ return [...COVERAGE_SOURCES, ...COVERAGE_EXCLUDES.map((glob) => `!${glob}`)]
21
+ }
22
+
23
+ /** The `sonar.coverage.exclusions` value that matches `collectCoverageFrom`; `sonar.exclusions` is `sonarExclusions()`. */
24
+ function sonarCoverageExclusions() {
25
+ return COVERAGE_ONLY_EXCLUSIONS.join(",")
26
+ }
27
+
28
+ function sonarExclusions() {
29
+ return SONAR_EXCLUSIONS.join(",")
30
+ }
31
+
32
+ const TS_TRANSFORM = String.raw`^.+\.ts$`
33
+
34
+ /**
35
+ * ts-jest without a type check (`diagnostics: false`; `isolatedModules: true` comes from @starci/tsconfig, which is where
36
+ * ts-jest 29.4 reads it — the ts-jest option of the same name is deprecated): types are checked once, by `typecheck` / `typecheck:e2e`, never per test file.
37
+ * `npm run test:e2e` must be `npm run typecheck:e2e && jest --selectProjects e2e` so nobody runs e2e on code that
38
+ * does not type-check.
39
+ */
40
+ function transform(tsconfig) {
41
+ return { [TS_TRANSFORM]: ["ts-jest", { tsconfig, diagnostics: false }] }
42
+ }
43
+
44
+ /**
45
+ * The whole jest config of a Nest repository: one `unit` project and one `e2e` project.
46
+ *
47
+ * @param {object} options
48
+ * @param {string} [options.tsconfig] ts-jest tsconfig, default `tsconfig.json`
49
+ * @param {Record<string,string|string[]>} [options.moduleNameMapper] path aliases for both projects
50
+ * @param {string[]} [options.roots] default `<rootDir>/src`, `<rootDir>/apps`
51
+ * @param {object} [options.unit] extra jest project options for unit (setupFiles, ...)
52
+ * @param {object} [options.e2e] extra jest project options for e2e (globalSetup, ...)
53
+ */
54
+ function starciJestConfig(options = {}) {
55
+ const tsconfig = options.tsconfig ?? "tsconfig.json"
56
+ const shared = {
57
+ preset: "ts-jest",
58
+ testEnvironment: "node",
59
+ rootDir: options.rootDir ?? ".",
60
+ roots: options.roots ?? ["<rootDir>/src", "<rootDir>/apps"],
61
+ moduleNameMapper: options.moduleNameMapper ?? {},
62
+ transform: transform(tsconfig),
63
+ }
64
+ const live = process.env.E2E_LIVE === "1"
65
+ return {
66
+ testTimeout: 120_000,
67
+ collectCoverageFrom: collectCoverageFrom(),
68
+ coverageDirectory: "coverage",
69
+ coverageReporters: ["lcov", "text-summary"],
70
+ projects: [
71
+ {
72
+ ...shared,
73
+ displayName: "unit",
74
+ clearMocks: true,
75
+ testMatch: ["**/*.spec.ts"],
76
+ testPathIgnorePatterns: ["/node_modules/", "\.e2e-spec\.ts$", "[\\/]src[\\/]tests[\\/]e2e[\\/]"],
77
+ ...options.unit,
78
+ },
79
+ {
80
+ ...shared,
81
+ displayName: "e2e",
82
+ maxWorkers: 1,
83
+ testMatch: ["<rootDir>/src/tests/e2e/**/*.e2e-spec.ts"],
84
+ testPathIgnorePatterns: live ? ["/node_modules/"] : ["/node_modules/", "[\\/]src[\\/]tests[\\/]e2e[\\/]live[\\/]"],
85
+ ...options.e2e,
86
+ },
87
+ ],
88
+ }
89
+ }
90
+
91
+ module.exports = {
92
+ starciJestConfig,
93
+ mock,
94
+ createMock,
95
+ collectCoverageFrom,
96
+ sonarCoverageExclusions,
97
+ sonarExclusions,
98
+ COVERAGE_SOURCES,
99
+ COVERAGE_EXCLUDES,
100
+ }
package/index.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import type { Config } from "jest"
2
+
3
+ export { mock, createMock, MockOf } from "./mock"
4
+
5
+ export interface StarciJestOptions {
6
+ rootDir?: string
7
+ tsconfig?: string
8
+ moduleNameMapper?: Record<string, string | string[]>
9
+ roots?: string[]
10
+ unit?: Record<string, unknown>
11
+ e2e?: Record<string, unknown>
12
+ }
13
+
14
+ export declare function starciJestConfig(options?: StarciJestOptions): Config
15
+ export declare function collectCoverageFrom(): string[]
16
+ export declare function sonarCoverageExclusions(): string
17
+ export declare function sonarExclusions(): string
18
+ export declare const COVERAGE_SOURCES: string[]
19
+ export declare const COVERAGE_EXCLUDES: string[]
package/mock.cjs ADDED
@@ -0,0 +1,53 @@
1
+ "use strict"
2
+
3
+ /**
4
+ * `mock<T>()` -- a typed test double that replaces the `{ ... } as unknown as T` cast.
5
+ *
6
+ * Every property read yields one stable `jest.fn()` (created on first read), so `mock<Repo>().find` is
7
+ * already a mock function, `expect(m.find).toHaveBeenCalledWith(...)` works, and `m.find.mockResolvedValue(x)`
8
+ * type-checks against `Repo["find"]`. Values passed in `overrides` win over the generated functions and may be
9
+ * partial: the return type is still `T`.
10
+ *
11
+ * `createMock` takes the function factory as an argument so the helper has no hard dependency on a global
12
+ * `jest`; `mock` binds it to `jest.fn`.
13
+ */
14
+
15
+ // Probed by promise resolution, jest matchers and serializers: they must read as absent, not as a mock function.
16
+ const ABSENT = new Set(["then", "asymmetricMatch", "$$typeof", "nodeType", "tagName", "toJSON"])
17
+
18
+ function createMock(makeFn) {
19
+ return function mock(overrides) {
20
+ const own = new Map()
21
+ const store = overrides ? { ...overrides } : {}
22
+ return new Proxy(store, {
23
+ get(target, property, receiver) {
24
+ if (Object.hasOwn(target, property)) return Reflect.get(target, property, receiver)
25
+ if (typeof property === "symbol" || property in Object.prototype) return Reflect.get(target, property, receiver)
26
+ if (ABSENT.has(property)) return undefined
27
+ if (!own.has(property)) own.set(property, makeFn())
28
+ return own.get(property)
29
+ },
30
+ set(target, property, value) {
31
+ own.delete(property)
32
+ target[property] = value
33
+ return true
34
+ },
35
+ has(target, property) {
36
+ return Object.hasOwn(target, property) || own.has(property)
37
+ },
38
+ })
39
+ }
40
+ }
41
+
42
+ // jest-runtime hands every module it loads, this one included, a module-scoped `jest` object; it is not a property
43
+ // of globalThis, so it is read as a free identifier. @jest/globals is the fallback for a runner that does not.
44
+ function jestFn() {
45
+ if (typeof jest !== "undefined" && typeof jest.fn === "function") return jest.fn()
46
+ try {
47
+ return require("@jest/globals").jest.fn()
48
+ } catch {
49
+ throw new Error("@starci/jest-preset mock<T>() needs the jest runtime; call it from a jest test")
50
+ }
51
+ }
52
+
53
+ module.exports = { mock: createMock(jestFn), createMock }
package/mock.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ /// <reference types="jest" />
2
+
3
+ /** Function members become jest mocks typed against the real signature; everything else stays as declared. */
4
+ export type MockOf<T> = T & {
5
+ [K in keyof T]: T[K] extends (...args: infer A) => infer R ? jest.Mock<R, A> & T[K] : T[K]
6
+ }
7
+
8
+ /** A typed test double for `T`. `overrides` may be partial; the result is usable wherever a `T` is required. */
9
+ export declare function mock<T extends object>(overrides?: Partial<T>): MockOf<T>
10
+
11
+ /** The factory behind `mock`, for callers that bring their own function factory (used by the package spec). */
12
+ export declare function createMock(makeFn: () => unknown): <T extends object>(overrides?: Partial<T>) => MockOf<T>
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@starci/jest-preset",
3
+ "version": "1.1.0",
4
+ "description": "The StarCi jest config for Nest repositories (unit and e2e projects, ts-jest with diagnostics off, coverage matching the Sonar denominators), the typed mock<T>() helper and the FakeClock test double for the platform/clock Clock port.",
5
+ "license": "UNLICENSED",
6
+ "private": false,
7
+ "main": "index.cjs",
8
+ "types": "index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./index.d.ts",
12
+ "default": "./index.cjs"
13
+ },
14
+ "./mock": {
15
+ "types": "./mock.d.ts",
16
+ "default": "./mock.cjs"
17
+ },
18
+ "./clock": {
19
+ "types": "./clock.d.ts",
20
+ "default": "./clock.cjs"
21
+ }
22
+ },
23
+ "files": [
24
+ "*.cjs",
25
+ "*.d.ts",
26
+ "!*.test.*",
27
+ "README.md"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "peerDependencies": {
33
+ "@types/jest": ">=29",
34
+ "jest": ">=29",
35
+ "ts-jest": ">=29"
36
+ },
37
+ "scripts": {
38
+ "test": "node --test \"*.test.mjs\""
39
+ }
40
+ }