@hublo/sentinel 1.4.0-alpha.9 → 1.4.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 +1 -0
- package/dist/bin/sentinel.js +45 -101
- package/dist/{chunk-7PUVK4YM.js → chunk-5VNYQIFD.js} +224 -13
- package/dist/chunk-5VNYQIFD.js.map +1 -0
- package/dist/chunk-BXXNV6NP.js +6411 -0
- package/dist/chunk-ELZHIN6E.js +16 -0
- package/dist/chunk-ELZHIN6E.js.map +1 -0
- package/dist/chunk-H3GSGAX2.js +11 -0
- package/dist/chunk-H3GSGAX2.js.map +1 -0
- package/dist/{chunk-WLFE5RUU.js → chunk-KMKQDGI6.js} +6 -8
- package/dist/chunk-KMKQDGI6.js.map +1 -0
- package/dist/{chunk-PWV3BMDA.js → chunk-MMKBDO5V.js} +11 -2
- package/dist/chunk-MMKBDO5V.js.map +1 -0
- package/dist/{chunk-L7WS36XV.js → chunk-MWNOFSYR.js} +221 -4427
- package/dist/chunk-NUOQXAYR.js +14 -0
- package/dist/chunk-NUOQXAYR.js.map +1 -0
- package/dist/chunk-O7REVMOC.js +38 -0
- package/dist/chunk-O7REVMOC.js.map +1 -0
- package/dist/chunk-QXFCZON7.js +16 -0
- package/dist/chunk-QXFCZON7.js.map +1 -0
- package/dist/{chunk-3TDUIKVQ.js → chunk-SWWQ7X7B.js} +11 -8
- package/dist/{chunk-3TDUIKVQ.js.map → chunk-SWWQ7X7B.js.map} +1 -1
- package/dist/{chunk-CPCUPK4J.js → chunk-Z7L4FGKP.js} +5 -12
- package/dist/chunk-Z7L4FGKP.js.map +1 -0
- package/dist/index.d.ts +28 -5
- package/dist/index.js +7 -3
- package/dist/roles/build/nest/toolchain.js +11 -33
- package/dist/roles/build/nest/toolchain.js.map +1 -1
- package/dist/roles/test/nest/toolchain.js +3 -2
- package/dist/roles/test/nest/toolchain.js.map +1 -1
- package/dist/roles/test/react/toolchain.js +3 -2
- package/dist/roles/test/react/toolchain.js.map +1 -1
- package/dist/roles/test/setup/a11y.d.ts +45 -0
- package/dist/roles/test/setup/a11y.js +72 -0
- package/dist/roles/test/setup/a11y.js.map +1 -0
- package/dist/roles/test/setup/file-boundary-close.d.ts +2 -0
- package/dist/roles/test/setup/file-boundary-close.js +8 -0
- package/dist/roles/test/setup/file-boundary-close.js.map +1 -0
- package/dist/roles/test/setup/file-boundary.d.ts +2 -0
- package/dist/roles/test/setup/file-boundary.js +62 -0
- package/dist/roles/test/setup/file-boundary.js.map +1 -0
- package/dist/roles/test/setup/jest-parity.js +19 -2
- package/dist/roles/test/setup/jest-parity.js.map +1 -1
- package/dist/roles/test/setup/msw-lifecycle.js +2 -1
- package/dist/roles/test/setup/msw-lifecycle.js.map +1 -1
- package/dist/roles/test/setup/msw-server.js +2 -1
- package/dist/roles/test/setup/msw-server.js.map +1 -1
- package/dist/roles/test/setup/w3c.d.ts +75 -0
- package/dist/roles/test/setup/w3c.js +49 -0
- package/dist/roles/test/setup/w3c.js.map +1 -0
- package/dist/roles/test/setup/workspace-entry.js +3 -2
- package/dist/roles/test/setup/workspace-entry.js.map +1 -1
- package/dist/roles/test/shared-test-config.d.ts +13 -0
- package/dist/roles/test/shared-test-config.js +3 -2
- package/dist/validate-BKD2ICT7.js +170 -0
- package/docs/test-adoption.md +282 -5
- package/docs/using-sentinel.md +17 -1
- package/docs/validating-a-change.md +35 -2
- package/package.json +25 -1
- package/types/jest-global.d.ts +85 -0
- package/types/mock-extended.d.ts +52 -0
- package/dist/chunk-7PUVK4YM.js.map +0 -1
- package/dist/chunk-CPCUPK4J.js.map +0 -1
- package/dist/chunk-PWV3BMDA.js.map +0 -1
- package/dist/chunk-WLFE5RUU.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/roles/test/nest/toolchain.ts","../../../../src/roles/test/nest/nest-test-config.ts"],"sourcesContent":["/**\n * What `@hublo/sentinel/test/nest` gives an adopted module.\n *\n * Its own entry point, like the build ones, so importing it from a config never drags the CLI and\n * its adapters into a test run.\n *\n * It differs from `test/react` in one way that matters, and the difference is not decoration.\n * React modules already had a Vitest config carrying decisions their owners made and justified, so\n * that preset is a pure re-export surface and moves nothing. Nest modules have NO Vitest config:\n * one has to be written, and four of its lines are load-bearing in ways nobody would guess from a\n * migration guide. Those four live in `nestTestConfig` rather than in 133 copies.\n */\n\n/**\n * From `vitest/config`, and NOT from `vite`, which also exports a `defineConfig`.\n *\n * They are different functions: `vitest/config`'s knows the `test` key, vite's does not. A config\n * built with vite's would load and quietly drop its entire test block, which is the kind of\n * failure that reads as \"vitest found no tests\" and sends someone hunting through globs.\n *\n * The BUILD role's nest entry point re-exports vite's, correctly for its purpose. Importing that\n * one here would be the exact mistake this comment exists to prevent.\n */\nexport { defineConfig, mergeConfig } from 'vitest/config'\n\n/**\n * From `vite`, because `vitest/config` does not export it. Two of the six already-Vitest configs\n * build their test env with it, and Nest services read their env the same way.\n *\n * Re-exported rather than claimed: the `vite` package belongs to the build role, and a module\n * adopting only this one keeps its own.\n */\nexport { loadEnv } from 'vite'\n\n/** The whole config, as one call. See `nest-test-config.ts` for why each line is there. */\nexport { nestTestConfig, type NestTestOptions } from './nest-test-config.js'\n\n/**\n * Re-exported so a module that needs to compose rather than override can reach them without\n * importing the BUILD role's entry point from a test config, which would pull vite's\n * `defineConfig` into scope beside vitest's and invite the mistake above.\n */\nexport {\n decoratorMetadata,\n type DecoratorMetadataOptions,\n} from '../../build/nest/decorator-metadata.js'\nexport { tsconfigAliases, type Alias } from '../../build/nest/tsconfig-aliases.js'\n\nexport type { ConfigEnv, TestUserConfig, ViteUserConfig } from 'vitest/config'\n","import type { ViteUserConfig } from 'vitest/config'\n\n/**\n * The Vitest config a NEST module gets: the shared one, plus the three things that are not shared.\n *\n * Everything load-bearing lives in `shared-test-config.ts`, because almost everything IS shared.\n * Measured across this repo before splitting, so the line falls where the code does and not where\n * the names suggest:\n *\n * jest-mock-extended front 21 nest 2542 shared\n * msw front 848 nest 517 shared\n * axios front 692 nest 863 shared\n * @prisma/ front 0 nest 1186 NEST ONLY\n *\n * So the nest side is three things: the generated Prisma client's runtime alias, `isolate` (a Nest\n * module registers metadata as an import side effect, so a shared module registry lets one suite\n * see what another registered), and the decorator plugin for the one module that needs it.\n */\nimport { sharedTestConfig, type SharedTestOptions } from '../shared-test-config.js'\n\nexport type { SharedTestOptions as NestTestOptions }\n\nexport function nestTestConfig(options: SharedTestOptions): ViteUserConfig {\n return sharedTestConfig({ ...options, flavour: 'nest' })\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../../src/roles/test/nest/toolchain.ts","../../../../src/roles/test/nest/nest-test-config.ts"],"sourcesContent":["/**\n * What `@hublo/sentinel/test/nest` gives an adopted module.\n *\n * Its own entry point, like the build ones, so importing it from a config never drags the CLI and\n * its adapters into a test run.\n *\n * It differs from `test/react` in one way that matters, and the difference is not decoration.\n * React modules already had a Vitest config carrying decisions their owners made and justified, so\n * that preset is a pure re-export surface and moves nothing. Nest modules have NO Vitest config:\n * one has to be written, and four of its lines are load-bearing in ways nobody would guess from a\n * migration guide. Those four live in `nestTestConfig` rather than in 133 copies.\n */\n\n/**\n * From `vitest/config`, and NOT from `vite`, which also exports a `defineConfig`.\n *\n * They are different functions: `vitest/config`'s knows the `test` key, vite's does not. A config\n * built with vite's would load and quietly drop its entire test block, which is the kind of\n * failure that reads as \"vitest found no tests\" and sends someone hunting through globs.\n *\n * The BUILD role's nest entry point re-exports vite's, correctly for its purpose. Importing that\n * one here would be the exact mistake this comment exists to prevent.\n */\nexport { defineConfig, mergeConfig } from 'vitest/config'\n\n/**\n * From `vite`, because `vitest/config` does not export it. Two of the six already-Vitest configs\n * build their test env with it, and Nest services read their env the same way.\n *\n * Re-exported rather than claimed: the `vite` package belongs to the build role, and a module\n * adopting only this one keeps its own.\n */\nexport { loadEnv } from 'vite'\n\n/** The whole config, as one call. See `nest-test-config.ts` for why each line is there. */\nexport { nestTestConfig, type NestTestOptions } from './nest-test-config.js'\n\n/**\n * Re-exported so a module that needs to compose rather than override can reach them without\n * importing the BUILD role's entry point from a test config, which would pull vite's\n * `defineConfig` into scope beside vitest's and invite the mistake above.\n */\nexport {\n decoratorMetadata,\n type DecoratorMetadataOptions,\n} from '../../build/nest/decorator-metadata.js'\nexport { tsconfigAliases, type Alias } from '../../build/nest/tsconfig-aliases.js'\n\nexport type { ConfigEnv, TestUserConfig, ViteUserConfig } from 'vitest/config'\n","import type { ViteUserConfig } from 'vitest/config'\n\n/**\n * The Vitest config a NEST module gets: the shared one, plus the three things that are not shared.\n *\n * Everything load-bearing lives in `shared-test-config.ts`, because almost everything IS shared.\n * Measured across this repo before splitting, so the line falls where the code does and not where\n * the names suggest:\n *\n * jest-mock-extended front 21 nest 2542 shared\n * msw front 848 nest 517 shared\n * axios front 692 nest 863 shared\n * @prisma/ front 0 nest 1186 NEST ONLY\n *\n * So the nest side is three things: the generated Prisma client's runtime alias, `isolate` (a Nest\n * module registers metadata as an import side effect, so a shared module registry lets one suite\n * see what another registered), and the decorator plugin for the one module that needs it.\n */\nimport { sharedTestConfig, type SharedTestOptions } from '../shared-test-config.js'\n\nexport type { SharedTestOptions as NestTestOptions }\n\nexport function nestTestConfig(options: SharedTestOptions): ViteUserConfig {\n return sharedTestConfig({ ...options, flavour: 'nest' })\n}\n"],"mappings":";;;;;;;;;;AAuBA,SAAS,cAAc,mBAAmB;AAS1C,SAAS,eAAe;;;ACVjB,SAAS,eAAe,SAA4C;AACzE,SAAO,iBAAiB,EAAE,GAAG,SAAS,SAAS,OAAO,CAAC;AACzD;","names":[]}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
jestExportConditions,
|
|
3
3
|
sharedTestConfig
|
|
4
|
-
} from "../../../chunk-
|
|
5
|
-
import "../../../chunk-
|
|
4
|
+
} from "../../../chunk-5VNYQIFD.js";
|
|
5
|
+
import "../../../chunk-SWWQ7X7B.js";
|
|
6
|
+
import "../../../chunk-QXFCZON7.js";
|
|
6
7
|
|
|
7
8
|
// src/roles/test/react/toolchain.ts
|
|
8
9
|
import { defineConfig, mergeConfig } from "vitest/config";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/roles/test/react/toolchain.ts","../../../../src/roles/test/react/react-test-config.ts"],"sourcesContent":["/**\n * What `@hublo/sentinel/test/react` gives an adopted module.\n *\n * A re-export surface, not a shape, and that is the measured answer rather than a preference.\n * The six configs already on Vitest fall into two groups: three React apps that converge on one\n * skeleton, and three modules of twelve to twenty lines that share almost nothing. A shared form\n * would serve half of them.\n *\n * More decisive: those configs carry decisions their owners made and justified in comments, a\n * `pool: 'threads'` measured on 1100 test files, an `experimental.fsModuleCache` measured against\n * transform and import time. Moving them is not a tooling migration, so the role moves the\n * imports and leaves every line of judgement where it is.\n *\n * Its own entry point, like the build ones, so importing it from a config never drags the CLI\n * and its adapters into a test run.\n */\n\n/**\n * From `vitest/config`, and NOT from `vite`, which also exports a `defineConfig`.\n *\n * They are different functions: measured, `vitest/config`'s `defineConfig` is not the same object\n * as vite's, and only it knows the `test` key. A config built with vite's would load and quietly\n * drop its entire test block. `mergeConfig` IS the same function in both, so its source does not\n * matter and it is taken from here for consistency.\n */\nexport { defineConfig, mergeConfig } from 'vitest/config'\n\n/**\n * From `vite`, because `vitest/config` does not export it. Measured, not assumed.\n *\n * Two of the six configs build their test env with it. Re-exported rather than claimed: the\n * `vite` package belongs to the build role, and a module adopting only this one keeps its own.\n */\nexport { loadEnv } from 'vite'\n\n/**\n * The names `vitest/config` actually exports, checked against its own declarations rather than\n * guessed: vite's `UserConfig` is re-exported there AS `ViteUserConfig`, and the test-side one is\n * `TestUserConfig`. Importing `UserConfig` from it compiles nowhere.\n */\nexport type { ConfigEnv, TestUserConfig, ViteUserConfig } from 'vitest/config'\n\n/**\n * The one thing this surface ADDS rather than re-exports, and only because a module cannot write\n * it for itself: Vite appends its own export conditions back over anything a config declares, so\n * restoring jest's resolution takes a plugin. Its own file carries the measurement.\n */\nexport { jestExportConditions } from './jest-export-conditions.js'\n\n/**\n * The config a MIGRATED React module gets, written by `--init --test`.\n *\n * Added on 23/09, because until then a front module being migrated received `nestTestConfig` while\n * the report announced `preset=react`. The re-export surface above is for the six modules that\n * already had a Vitest config and keep their own decisions; this is for the ones that had none and\n * need one written.\n */\nexport { reactTestConfig, type ReactTestOptions } from './react-test-config.js'\n","import type { ViteUserConfig } from 'vitest/config'\n\n/**\n * The Vitest config a REACT module gets: the shared one, without the Nest-only parts.\n *\n * ⚠️ This exists because a front module was being migrated with `nestTestConfig`, and the report\n * then said `preset=react` while the file it had just written imported `test/nest`. A tool that\n * contradicts itself in the same run is worse than one that is simply incomplete.\n *\n * What it does NOT carry, each measured rather than assumed: the `@prisma/*` runtime alias (1186\n * files mention it under nest, ZERO under front) and `isolate` (Nest registers metadata as an\n * import side effect; React has no such catalog, and isolation is not free).\n *\n * What it DOES carry is everything else, which is most of it: the workspace tsconfig path aliases,\n * jest's export conditions, the four test-file extensions, the concurrency this repo runs with, and\n * the msw, axios and jest-mock-extended aliases, all three of which the front uses heavily.\n *\n * Deliberately NOT here: a React plugin. Vite's own transform handles JSX in a test run, and the\n * front suites pass without one, so adding it would be paying for something nothing asked for.\n */\nimport { sharedTestConfig, type SharedTestOptions } from '../shared-test-config.js'\n\nexport type { SharedTestOptions as ReactTestOptions }\n\nexport function reactTestConfig(options: SharedTestOptions): ViteUserConfig {\n return sharedTestConfig({ ...options, flavour: 'react' })\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../../src/roles/test/react/toolchain.ts","../../../../src/roles/test/react/react-test-config.ts"],"sourcesContent":["/**\n * What `@hublo/sentinel/test/react` gives an adopted module.\n *\n * A re-export surface, not a shape, and that is the measured answer rather than a preference.\n * The six configs already on Vitest fall into two groups: three React apps that converge on one\n * skeleton, and three modules of twelve to twenty lines that share almost nothing. A shared form\n * would serve half of them.\n *\n * More decisive: those configs carry decisions their owners made and justified in comments, a\n * `pool: 'threads'` measured on 1100 test files, an `experimental.fsModuleCache` measured against\n * transform and import time. Moving them is not a tooling migration, so the role moves the\n * imports and leaves every line of judgement where it is.\n *\n * Its own entry point, like the build ones, so importing it from a config never drags the CLI\n * and its adapters into a test run.\n */\n\n/**\n * From `vitest/config`, and NOT from `vite`, which also exports a `defineConfig`.\n *\n * They are different functions: measured, `vitest/config`'s `defineConfig` is not the same object\n * as vite's, and only it knows the `test` key. A config built with vite's would load and quietly\n * drop its entire test block. `mergeConfig` IS the same function in both, so its source does not\n * matter and it is taken from here for consistency.\n */\nexport { defineConfig, mergeConfig } from 'vitest/config'\n\n/**\n * From `vite`, because `vitest/config` does not export it. Measured, not assumed.\n *\n * Two of the six configs build their test env with it. Re-exported rather than claimed: the\n * `vite` package belongs to the build role, and a module adopting only this one keeps its own.\n */\nexport { loadEnv } from 'vite'\n\n/**\n * The names `vitest/config` actually exports, checked against its own declarations rather than\n * guessed: vite's `UserConfig` is re-exported there AS `ViteUserConfig`, and the test-side one is\n * `TestUserConfig`. Importing `UserConfig` from it compiles nowhere.\n */\nexport type { ConfigEnv, TestUserConfig, ViteUserConfig } from 'vitest/config'\n\n/**\n * The one thing this surface ADDS rather than re-exports, and only because a module cannot write\n * it for itself: Vite appends its own export conditions back over anything a config declares, so\n * restoring jest's resolution takes a plugin. Its own file carries the measurement.\n */\nexport { jestExportConditions } from './jest-export-conditions.js'\n\n/**\n * The config a MIGRATED React module gets, written by `--init --test`.\n *\n * Added on 23/09, because until then a front module being migrated received `nestTestConfig` while\n * the report announced `preset=react`. The re-export surface above is for the six modules that\n * already had a Vitest config and keep their own decisions; this is for the ones that had none and\n * need one written.\n */\nexport { reactTestConfig, type ReactTestOptions } from './react-test-config.js'\n","import type { ViteUserConfig } from 'vitest/config'\n\n/**\n * The Vitest config a REACT module gets: the shared one, without the Nest-only parts.\n *\n * ⚠️ This exists because a front module was being migrated with `nestTestConfig`, and the report\n * then said `preset=react` while the file it had just written imported `test/nest`. A tool that\n * contradicts itself in the same run is worse than one that is simply incomplete.\n *\n * What it does NOT carry, each measured rather than assumed: the `@prisma/*` runtime alias (1186\n * files mention it under nest, ZERO under front) and `isolate` (Nest registers metadata as an\n * import side effect; React has no such catalog, and isolation is not free).\n *\n * What it DOES carry is everything else, which is most of it: the workspace tsconfig path aliases,\n * jest's export conditions, the four test-file extensions, the concurrency this repo runs with, and\n * the msw, axios and jest-mock-extended aliases, all three of which the front uses heavily.\n *\n * Deliberately NOT here: a React plugin. Vite's own transform handles JSX in a test run, and the\n * front suites pass without one, so adding it would be paying for something nothing asked for.\n */\nimport { sharedTestConfig, type SharedTestOptions } from '../shared-test-config.js'\n\nexport type { SharedTestOptions as ReactTestOptions }\n\nexport function reactTestConfig(options: SharedTestOptions): ViteUserConfig {\n return sharedTestConfig({ ...options, flavour: 'react' })\n}\n"],"mappings":";;;;;;;;AAyBA,SAAS,cAAc,mBAAmB;AAQ1C,SAAS,eAAe;;;ACTjB,SAAS,gBAAgB,SAA4C;AAC1E,SAAO,iBAAiB,EAAE,GAAG,SAAS,SAAS,QAAQ,CAAC;AAC1D;","names":[]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { RunOptions } from 'axe-core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `expect(element).toBeAccessible()`, run by axe-core directly.
|
|
5
|
+
*
|
|
6
|
+
* Opt in from a module's config:
|
|
7
|
+
*
|
|
8
|
+
* setupFiles: ['@hublo/sentinel/test/setup/a11y']
|
|
9
|
+
*
|
|
10
|
+
* ## Why axe-core DIRECTLY, and not jest-axe or vitest-axe
|
|
11
|
+
*
|
|
12
|
+
* Both are thin wrappers whose job is to turn axe's result into a matcher, which is the twenty
|
|
13
|
+
* lines below. What they add on top is a dependency that lags axe itself, its own idea of which
|
|
14
|
+
* rules to run, and a second place where a violation can be filtered out. Measured while choosing:
|
|
15
|
+
* the RULE SET decides the outcome far more than the runner does, 12 messages against 70 on the
|
|
16
|
+
* same markup depending only on configuration. So the configuration is the thing worth owning, and
|
|
17
|
+
* a wrapper that owns it for us is a wrapper that decides for the modules.
|
|
18
|
+
*
|
|
19
|
+
* axe-core is also the engine those wrappers call, so this is not a reimplementation, it is the
|
|
20
|
+
* same engine with one fewer layer between the module and its rules.
|
|
21
|
+
*
|
|
22
|
+
* ## What it asserts, and what it deliberately does not
|
|
23
|
+
*
|
|
24
|
+
* It runs axe on the element it is given and fails on any VIOLATION. It says nothing about
|
|
25
|
+
* "incomplete" results, which axe reports when it cannot decide without a real browser: colour
|
|
26
|
+
* contrast against a computed background, for instance. Under jsdom those are not inconclusive by
|
|
27
|
+
* accident, they are inconclusive by construction, and failing on them would teach everyone to
|
|
28
|
+
* disable the matcher.
|
|
29
|
+
*
|
|
30
|
+
* ⚠️ So a green `toBeAccessible()` means "axe found no violation it could decide here", never "this
|
|
31
|
+
* is accessible". The message says so on failure, and the count of incomplete checks is reported
|
|
32
|
+
* alongside, so nobody reads silence as coverage.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
declare module 'vitest' {
|
|
36
|
+
interface Matchers<T = any> {
|
|
37
|
+
/**
|
|
38
|
+
* Run axe-core on this element and fail on any violation.
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ A pass means axe found no violation it could DECIDE under jsdom. Checks axe reports as
|
|
41
|
+
* incomplete, such as colour contrast, are counted in the message and never failed.
|
|
42
|
+
*/
|
|
43
|
+
toBeAccessible: (options?: RunOptions) => Promise<T>;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/roles/test/setup/a11y.ts
|
|
2
|
+
import { expect } from "vitest";
|
|
3
|
+
function describeViolation(violation) {
|
|
4
|
+
const nodes = violation.nodes.slice(0, 3).map((node) => ` ${node.target.join(" ")}
|
|
5
|
+
${node.failureSummary ?? ""}`).join("\n");
|
|
6
|
+
const more = violation.nodes.length > 3 ? `
|
|
7
|
+
\u2026and ${violation.nodes.length - 3} more element(s)` : "";
|
|
8
|
+
return ` ${violation.id} (${violation.impact ?? "unknown impact"}): ${violation.help}
|
|
9
|
+
${violation.helpUrl}
|
|
10
|
+
${nodes}${more}`;
|
|
11
|
+
}
|
|
12
|
+
var engine;
|
|
13
|
+
var NODE = { ELEMENT: 1, DOCUMENT: 9, FRAGMENT: 11 };
|
|
14
|
+
function isDomNode(value) {
|
|
15
|
+
return typeof value === "object" && value !== null && "nodeType" in value;
|
|
16
|
+
}
|
|
17
|
+
function nodeTypeOf(value) {
|
|
18
|
+
return isDomNode(value) ? value.nodeType : void 0;
|
|
19
|
+
}
|
|
20
|
+
function toAxeContext(target) {
|
|
21
|
+
if (nodeTypeOf(target) !== NODE.FRAGMENT) return target;
|
|
22
|
+
const elements = Array.from(target.childNodes).filter(
|
|
23
|
+
(node) => node.nodeType === NODE.ELEMENT
|
|
24
|
+
);
|
|
25
|
+
return elements.length > 0 ? { include: elements } : { include: [target] };
|
|
26
|
+
}
|
|
27
|
+
function assertSupportedDom() {
|
|
28
|
+
const agent = typeof navigator === "undefined" ? "" : (navigator.userAgent ?? "").toLowerCase();
|
|
29
|
+
if (agent.includes("happy-dom")) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`toBeAccessible() needs the "jsdom" environment: axe-core does not support happy-dom. Set \`environment: 'jsdom'\` for this suite, or for this file with \`// @vitest-environment jsdom\`.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function run(target, options) {
|
|
36
|
+
engine ??= import("axe-core");
|
|
37
|
+
const loaded = await engine;
|
|
38
|
+
const axe = loaded.default ?? loaded;
|
|
39
|
+
return axe.run(toAxeContext(target), {
|
|
40
|
+
elementRef: false,
|
|
41
|
+
...options
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
expect.extend({
|
|
45
|
+
async toBeAccessible(received, options) {
|
|
46
|
+
assertSupportedDom();
|
|
47
|
+
if (isDomNode(received) && received.isConnected === false) {
|
|
48
|
+
return {
|
|
49
|
+
pass: false,
|
|
50
|
+
message: () => `toBeAccessible() needs a node that is IN the document: axe analyses the page, and this one is detached. Testing Library's \`asFragment()\` returns a detached fragment, so pass \`container\` instead: expect(render(<X />).container).toBeAccessible().`
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const type = nodeTypeOf(received);
|
|
54
|
+
if (type !== NODE.ELEMENT && type !== NODE.DOCUMENT && type !== NODE.FRAGMENT) {
|
|
55
|
+
return {
|
|
56
|
+
pass: false,
|
|
57
|
+
message: () => `toBeAccessible() needs an Element, a Document or a DocumentFragment, and received ${typeof received}. From Testing Library, pass the container: expect(render(<X />).container).toBeAccessible(), or asFragment().`
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const results = await run(received, options);
|
|
61
|
+
const incomplete = results.incomplete.length === 0 ? "" : `
|
|
62
|
+
|
|
63
|
+
${results.incomplete.length} check(s) were INCOMPLETE and are not failures: axe cannot decide them without a real browser (contrast against a computed background, for instance). A pass here means no decidable violation, never "this is accessible".`;
|
|
64
|
+
return {
|
|
65
|
+
pass: results.violations.length === 0,
|
|
66
|
+
message: () => results.violations.length === 0 ? `expected accessibility violations, and axe found none.${incomplete}` : `${results.violations.length} accessibility violation(s):
|
|
67
|
+
|
|
68
|
+
${results.violations.map(describeViolation).join("\n\n")}${incomplete}`
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
//# sourceMappingURL=a11y.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/roles/test/setup/a11y.ts"],"sourcesContent":["/**\n * `expect(element).toBeAccessible()`, run by axe-core directly.\n *\n * Opt in from a module's config:\n *\n * setupFiles: ['@hublo/sentinel/test/setup/a11y']\n *\n * ## Why axe-core DIRECTLY, and not jest-axe or vitest-axe\n *\n * Both are thin wrappers whose job is to turn axe's result into a matcher, which is the twenty\n * lines below. What they add on top is a dependency that lags axe itself, its own idea of which\n * rules to run, and a second place where a violation can be filtered out. Measured while choosing:\n * the RULE SET decides the outcome far more than the runner does, 12 messages against 70 on the\n * same markup depending only on configuration. So the configuration is the thing worth owning, and\n * a wrapper that owns it for us is a wrapper that decides for the modules.\n *\n * axe-core is also the engine those wrappers call, so this is not a reimplementation, it is the\n * same engine with one fewer layer between the module and its rules.\n *\n * ## What it asserts, and what it deliberately does not\n *\n * It runs axe on the element it is given and fails on any VIOLATION. It says nothing about\n * \"incomplete\" results, which axe reports when it cannot decide without a real browser: colour\n * contrast against a computed background, for instance. Under jsdom those are not inconclusive by\n * accident, they are inconclusive by construction, and failing on them would teach everyone to\n * disable the matcher.\n *\n * ⚠️ So a green `toBeAccessible()` means \"axe found no violation it could decide here\", never \"this\n * is accessible\". The message says so on failure, and the count of incomplete checks is reported\n * alongside, so nobody reads silence as coverage.\n */\n/// <reference lib=\"dom\" />\nimport type { AxeResults, Result, RunOptions } from 'axe-core'\nimport { expect } from 'vitest'\n\n/** One violation, rendered the way a developer can act on it. */\nfunction describeViolation(violation: Result): string {\n const nodes = violation.nodes\n .slice(0, 3)\n .map((node) => ` ${node.target.join(' ')}\\n ${node.failureSummary ?? ''}`)\n .join('\\n')\n const more =\n violation.nodes.length > 3 ? `\\n …and ${violation.nodes.length - 3} more element(s)` : ''\n return (\n ` ${violation.id} (${violation.impact ?? 'unknown impact'}): ${violation.help}\\n` +\n ` ${violation.helpUrl}\\n${nodes}${more}`\n )\n}\n\n/**\n * ⚠️ The engine is loaded on FIRST USE, not when this file is read.\n *\n * `setupFiles` runs per test FILE under Vitest's isolation, and axe costs 28 ms to import. On\n * `host-admin`, 1460 files, that is 41 seconds added to every run for a matcher almost no file\n * calls. Lazily, a suite that never asserts on accessibility pays nothing at all, and one that\n * does pays once per file that does.\n *\n * The type import above is erased at build time, so it costs nothing either.\n */\nlet engine: Promise<typeof import('axe-core')> | undefined\n\n/** `nodeType`, because that is the one thing every DOM implementation agrees on. */\nconst NODE = { ELEMENT: 1, DOCUMENT: 9, FRAGMENT: 11 } as const\n\n/**\n * ⚠️ `nodeType`, NOT `instanceof Element`, and the difference is not style.\n *\n * `instanceof` compares against the constructor of THIS realm. A DOM built in another one, which\n * is what happens with jsdom inside a Vitest worker, fails the check while being a perfectly good\n * element, and the matcher then refuses the container it was handed. Taken from the OVH manager\n * kit, which hit it first.\n */\nfunction isDomNode(value: unknown): value is Element | Document | DocumentFragment {\n return typeof value === 'object' && value !== null && 'nodeType' in value\n}\n\nfunction nodeTypeOf(value: unknown): number | undefined {\n return isDomNode(value) ? (value as { nodeType: number }).nodeType : undefined\n}\n\n/**\n * The context axe accepts, from whatever Testing Library handed over.\n *\n * ⚠️ `asFragment()` returns a DocumentFragment, which axe cannot take directly: it is turned into\n * the list of its element children, which is the shape axe documents for a partial context.\n */\nfunction toAxeContext(target: Element | Document | DocumentFragment): unknown {\n if (nodeTypeOf(target) !== NODE.FRAGMENT) return target\n const elements = Array.from((target as DocumentFragment).childNodes).filter(\n (node) => (node as { nodeType: number }).nodeType === NODE.ELEMENT,\n )\n return elements.length > 0 ? { include: elements } : { include: [target] }\n}\n\n/**\n * ⚠️ axe does not work under happy-dom, and says nothing useful when it does not.\n *\n * Refused here with a sentence that names the fix, rather than leaving someone to read an\n * incomprehensible axe error. This repo has an open question about moving to happy-dom, which is\n * exactly why the guard is worth having before that question is answered. Taken from the OVH\n * manager kit.\n */\nfunction assertSupportedDom(): void {\n const agent = typeof navigator === 'undefined' ? '' : (navigator.userAgent ?? '').toLowerCase()\n if (agent.includes('happy-dom')) {\n throw new Error(\n `toBeAccessible() needs the \"jsdom\" environment: axe-core does not support happy-dom. ` +\n `Set \\`environment: 'jsdom'\\` for this suite, or for this file with ` +\n `\\`// @vitest-environment jsdom\\`.`,\n )\n }\n}\n\nasync function run(\n target: Element | Document | DocumentFragment,\n options?: RunOptions,\n): Promise<AxeResults> {\n engine ??= import('axe-core')\n const loaded = await engine\n // The package is CJS with a default export under ESM interop, and a namespace otherwise.\n const axe = (loaded as { default?: typeof loaded }).default ?? loaded\n /*\n * `elementRef: false` because the handle axe would attach is useless once the test has torn the\n * DOM down, and it keeps the result serialisable, which is what lets a reporter print it.\n */\n return axe.run(toAxeContext(target) as Parameters<typeof axe.run>[0], {\n elementRef: false,\n ...options,\n })\n}\n\nexpect.extend({\n async toBeAccessible(received: unknown, options?: RunOptions) {\n assertSupportedDom()\n\n /*\n * ⚠️ axe analyses the PAGE, so a node that is not in it has nothing to analyse, and what axe\n * says about that is `No elements found for include in page Context`, which names neither the\n * cause nor the fix. Measured on a detached `DocumentFragment`, which is exactly what\n * Testing Library's `asFragment()` returns.\n */\n if (isDomNode(received) && (received as { isConnected?: boolean }).isConnected === false) {\n return {\n pass: false,\n message: () =>\n `toBeAccessible() needs a node that is IN the document: axe analyses the page, and this ` +\n `one is detached. Testing Library's \\`asFragment()\\` returns a detached fragment, so ` +\n `pass \\`container\\` instead: expect(render(<X />).container).toBeAccessible().`,\n }\n }\n\n const type = nodeTypeOf(received)\n if (type !== NODE.ELEMENT && type !== NODE.DOCUMENT && type !== NODE.FRAGMENT) {\n return {\n pass: false,\n message: () =>\n `toBeAccessible() needs an Element, a Document or a DocumentFragment, and received ` +\n `${typeof received}. From Testing Library, pass the container: ` +\n `expect(render(<X />).container).toBeAccessible(), or asFragment().`,\n }\n }\n\n const results = await run(received as Element | Document | DocumentFragment, options)\n const incomplete =\n results.incomplete.length === 0\n ? ''\n : `\\n\\n${results.incomplete.length} check(s) were INCOMPLETE and are not failures: axe ` +\n `cannot decide them without a real browser (contrast against a computed background, for ` +\n `instance). A pass here means no decidable violation, never \"this is accessible\".`\n\n return {\n pass: results.violations.length === 0,\n message: () =>\n results.violations.length === 0\n ? `expected accessibility violations, and axe found none.${incomplete}`\n : `${results.violations.length} accessibility violation(s):\\n\\n` +\n `${results.violations.map(describeViolation).join('\\n\\n')}${incomplete}`,\n }\n },\n})\n\ndeclare module 'vitest' {\n // `T = any` to match Vitest's own declaration: TypeScript refuses to merge an interface whose\n // type parameters differ, and the message it gives points at this line rather than at the cause.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n interface Matchers<T = any> {\n /**\n * Run axe-core on this element and fail on any violation.\n *\n * ⚠️ A pass means axe found no violation it could DECIDE under jsdom. Checks axe reports as\n * incomplete, such as colour contrast, are counted in the message and never failed.\n */\n toBeAccessible: (options?: RunOptions) => Promise<T>\n }\n}\n"],"mappings":";AAiCA,SAAS,cAAc;AAGvB,SAAS,kBAAkB,WAA2B;AACpD,QAAM,QAAQ,UAAU,MACrB,MAAM,GAAG,CAAC,EACV,IAAI,CAAC,SAAS,SAAS,KAAK,OAAO,KAAK,GAAG,CAAC;AAAA,UAAa,KAAK,kBAAkB,EAAE,EAAE,EACpF,KAAK,IAAI;AACZ,QAAM,OACJ,UAAU,MAAM,SAAS,IAAI;AAAA,kBAAgB,UAAU,MAAM,SAAS,CAAC,qBAAqB;AAC9F,SACE,KAAK,UAAU,EAAE,KAAK,UAAU,UAAU,gBAAgB,MAAM,UAAU,IAAI;AAAA,MACvE,UAAU,OAAO;AAAA,EAAK,KAAK,GAAG,IAAI;AAE7C;AAYA,IAAI;AAGJ,IAAM,OAAO,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG;AAUrD,SAAS,UAAU,OAAgE;AACjF,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,cAAc;AACtE;AAEA,SAAS,WAAW,OAAoC;AACtD,SAAO,UAAU,KAAK,IAAK,MAA+B,WAAW;AACvE;AAQA,SAAS,aAAa,QAAwD;AAC5E,MAAI,WAAW,MAAM,MAAM,KAAK,SAAU,QAAO;AACjD,QAAM,WAAW,MAAM,KAAM,OAA4B,UAAU,EAAE;AAAA,IACnE,CAAC,SAAU,KAA8B,aAAa,KAAK;AAAA,EAC7D;AACA,SAAO,SAAS,SAAS,IAAI,EAAE,SAAS,SAAS,IAAI,EAAE,SAAS,CAAC,MAAM,EAAE;AAC3E;AAUA,SAAS,qBAA2B;AAClC,QAAM,QAAQ,OAAO,cAAc,cAAc,MAAM,UAAU,aAAa,IAAI,YAAY;AAC9F,MAAI,MAAM,SAAS,WAAW,GAAG;AAC/B,UAAM,IAAI;AAAA,MACR;AAAA,IAGF;AAAA,EACF;AACF;AAEA,eAAe,IACb,QACA,SACqB;AACrB,aAAW,OAAO,UAAU;AAC5B,QAAM,SAAS,MAAM;AAErB,QAAM,MAAO,OAAuC,WAAW;AAK/D,SAAO,IAAI,IAAI,aAAa,MAAM,GAAoC;AAAA,IACpE,YAAY;AAAA,IACZ,GAAG;AAAA,EACL,CAAC;AACH;AAEA,OAAO,OAAO;AAAA,EACZ,MAAM,eAAe,UAAmB,SAAsB;AAC5D,uBAAmB;AAQnB,QAAI,UAAU,QAAQ,KAAM,SAAuC,gBAAgB,OAAO;AACxF,aAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS,MACP;AAAA,MAGJ;AAAA,IACF;AAEA,UAAM,OAAO,WAAW,QAAQ;AAChC,QAAI,SAAS,KAAK,WAAW,SAAS,KAAK,YAAY,SAAS,KAAK,UAAU;AAC7E,aAAO;AAAA,QACL,MAAM;AAAA,QACN,SAAS,MACP,qFACG,OAAO,QAAQ;AAAA,MAEtB;AAAA,IACF;AAEA,UAAM,UAAU,MAAM,IAAI,UAAmD,OAAO;AACpF,UAAM,aACJ,QAAQ,WAAW,WAAW,IAC1B,KACA;AAAA;AAAA,EAAO,QAAQ,WAAW,MAAM;AAItC,WAAO;AAAA,MACL,MAAM,QAAQ,WAAW,WAAW;AAAA,MACpC,SAAS,MACP,QAAQ,WAAW,WAAW,IAC1B,yDAAyD,UAAU,KACnE,GAAG,QAAQ,WAAW,MAAM;AAAA;AAAA,EACzB,QAAQ,WAAW,IAAI,iBAAiB,EAAE,KAAK,MAAM,CAAC,GAAG,UAAU;AAAA,IAC9E;AAAA,EACF;AACF,CAAC;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/roles/test/setup/file-boundary-close.ts"],"sourcesContent":["/**\n * The second half of the file boundary, and the reason it is a second entry at all.\n *\n * Placed LAST in `setupFiles`, after the module's own, so that by the time it runs:\n *\n * - every mock a setup file built is already recorded as infrastructure, and anything built from\n * here on belongs to the test file;\n * - every global a setup file installed is already part of the baseline, and anything added from\n * here on belongs to the test file.\n *\n * Neither could be decided from the first entry. Vitest offers no hook between \"the last setup\n * file finished\" and \"the test file is imported\", and a microtask does not help because setup\n * files are awaited in turn. The order of `setupFiles` is the only way to say \"after the others\",\n * so the boundary is opened by `./file-boundary.js` and closed here.\n */\nimport { PHASE } from './file-boundary-state.js'\n\nconst state = globalThis as Record<symbol, unknown>\n\nstate[PHASE] = 'file'\n\n/*\n * ⚠️ NO global sweep here, and that is a retraction.\n *\n * An earlier version snapshotted `Reflect.ownKeys(globalThis)` per file and deleted whatever a\n * file added. It fixed three tests on `front-components` that read a `window.google` left by\n * another file, and it looked principled: a fresh environment had none of those keys either.\n *\n * It is not the same thing. The other four boundaries RESTORE what the runner used to reset. This\n * one DELETED state belonging to whoever created it, and a library that initialises once per\n * worker never gets it back. Measured on `agency`: `@prisma/*` sets `globalThis.DEBUG` with `??=`\n * when its runtime is first imported, the sweep removed it at the end of that file, and 41 suites\n * then died on `Cannot read properties of undefined (reading 'split')`. The same run is green with\n * isolation, so it was ours. Logging every deletion over a `front-components` run had already said\n * as much and I read it as acceptable: 220 deletions, of which 210 were msw's interceptor symbols.\n *\n * Removing it costs ONE test on `front-components`, and that number is measured, not predicted:\n * an earlier version of this comment claimed 1418 green before the run had happened, and the run\n * said 1417. The survivor is `useFeatureFlags`, and it is not a global a test wrote. It is msw:\n * the sweep was deleting `Symbol(setup-server)` and the three interceptor symbols beside it 210\n * times a run, which forced msw to rebuild per file and hid the fact that its registry carries\n * state from one file to the next. Narrowing the sweep to string keys loses the same single test,\n * which is what pointed at msw in the first place.\n *\n * So the msw leak is real and open, and it belongs to msw's lifecycle rather than to a blanket\n * sweep of the global object. A test that writes to `globalThis` and does not clean up stays the\n * module's to fix, and `--validate` names it.\n */\n"],"mappings":";;;;;AAiBA,IAAM,QAAQ;AAEd,MAAM,KAAK,IAAI;","names":[]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CURRENT_FILE_MOCKS,
|
|
3
|
+
PHASE,
|
|
4
|
+
SHARED_WORKER
|
|
5
|
+
} from "../../../chunk-H3GSGAX2.js";
|
|
6
|
+
import {
|
|
7
|
+
alreadyInstalled
|
|
8
|
+
} from "../../../chunk-NUOQXAYR.js";
|
|
9
|
+
import {
|
|
10
|
+
fromModule
|
|
11
|
+
} from "../../../chunk-ELZHIN6E.js";
|
|
12
|
+
|
|
13
|
+
// src/roles/test/setup/file-boundary.ts
|
|
14
|
+
import { afterAll, afterEach, vi } from "vitest";
|
|
15
|
+
var state = globalThis;
|
|
16
|
+
state[SHARED_WORKER] = true;
|
|
17
|
+
var body = globalThis.document?.body;
|
|
18
|
+
if (body !== void 0) {
|
|
19
|
+
body.innerHTML = "";
|
|
20
|
+
body.removeAttribute("style");
|
|
21
|
+
}
|
|
22
|
+
var testingLibrary = fromModule("@testing-library/react");
|
|
23
|
+
if (typeof testingLibrary?.cleanup === "function") afterEach(testingLibrary.cleanup);
|
|
24
|
+
var mocksInThisFile = /* @__PURE__ */ new Set();
|
|
25
|
+
state[CURRENT_FILE_MOCKS] = mocksInThisFile;
|
|
26
|
+
state[PHASE] = "infrastructure";
|
|
27
|
+
var api = vi;
|
|
28
|
+
if (!alreadyInstalled(vi, "file-boundary")) {
|
|
29
|
+
for (const factory of ["fn", "spyOn"]) {
|
|
30
|
+
const create = api[factory].bind(api);
|
|
31
|
+
api[factory] = (...args) => {
|
|
32
|
+
const mock = create(...args);
|
|
33
|
+
register(mock);
|
|
34
|
+
return mock;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
for (const [name, method] of [
|
|
38
|
+
["clearAllMocks", "mockClear"],
|
|
39
|
+
["resetAllMocks", "mockReset"],
|
|
40
|
+
["restoreAllMocks", "mockRestore"]
|
|
41
|
+
]) {
|
|
42
|
+
api[name] = () => {
|
|
43
|
+
for (const mock of currentFileMocks()) mock[method]?.();
|
|
44
|
+
return api;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
afterAll(() => {
|
|
49
|
+
for (const mock of mocksInThisFile) mock.mockRestore?.();
|
|
50
|
+
mocksInThisFile.clear();
|
|
51
|
+
vi.useRealTimers();
|
|
52
|
+
vi.unstubAllGlobals();
|
|
53
|
+
vi.unstubAllEnvs();
|
|
54
|
+
});
|
|
55
|
+
function register(mock) {
|
|
56
|
+
if (state[PHASE] === "infrastructure") return;
|
|
57
|
+
state[CURRENT_FILE_MOCKS]?.add(mock);
|
|
58
|
+
}
|
|
59
|
+
function currentFileMocks() {
|
|
60
|
+
return state[CURRENT_FILE_MOCKS] ?? /* @__PURE__ */ new Set();
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=file-boundary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/roles/test/setup/file-boundary.ts"],"sourcesContent":["/**\n * The end of a test file, given back its meaning when Vitest is told not to isolate.\n *\n * ## What isolation was paying for\n *\n * With `isolate: true`, which is Vitest's default, every test file gets its own module registry\n * and its own environment. A file that patches a prototype, leaves fake timers installed, writes\n * `window.google` or empties `document.body` cannot reach the next file, because the next file\n * does not share the things it touched. Nobody writes that down, and no test asserts it: it is\n * bought silently, and it is the single most expensive thing a Vitest run does. Measured on\n * `front-components`, 230 files and 1418 tests, on the same machine and in the same session:\n *\n * jest, files in parallel 15.9 s\n * Vitest, isolated (today) 44.6 s\n * Vitest, this boundary, not isolated 13.0 s\n *\n * So the runner is not slower than jest. Re-evaluating the import graph once per file is, and that\n * is exactly what isolation asks for.\n *\n * ## Why a setup file is the only place this can live\n *\n * Measured: Vitest re-executes each `setupFiles` entry for EVERY test file, even with isolation\n * off, while an ordinary imported module is evaluated once per worker. So a hook registered here\n * re-arms per file, and `afterAll` here IS the end-of-file boundary. A hook registered at the\n * import time of a normal module arms once and never again, which is the whole bug behind\n * Testing Library's cleanup below.\n *\n * ## The five things a file must give back\n *\n * Each line was found by bisecting a real failure on `front-components`, never by guessing:\n *\n * | given back | what leaked without it |\n * | ------------------------------ | ---------------------------------------------------------- |\n * | Testing Library's `cleanup` | it registers at IMPORT time, so it armed for one file only |\n * | an empty `document` | a test writing `document.body.innerHTML`, and focus with it |\n * | spies | `vi.spyOn(Element.prototype, 'scrollIntoView')`, left on |\n * | real timers | a file installing fake timers and never restoring them |\n * | the file's own mock register | `vi.resetAllMocks()` reaching a shared fixture |\n *\n * The global object is given back by `./file-boundary-close.js`, which also closes the\n * infrastructure phase this file opens. The two are one mechanism in two entries because the\n * closing one has to run AFTER the module's own setup files, and `setupFiles` order is the only\n * way to say that.\n *\n * ## What this deliberately does NOT do\n *\n * Not a blanket `vi.restoreAllMocks()` at the boundary. Measured: it also resets the\n * `vi.fn().mockImplementation(...)` that a shared fixture exports at module scope, and 30 tests\n * then fail with `observe is not a function`. Isolation RE-CREATED that fixture, it did not\n * restore it, and those are different actions.\n *\n * Not a substitute for isolating a file that calls `vi.mock`. A module already evaluated in the\n * worker cannot be un-evaluated, so a mock declared by a later file cannot reach it. Those files\n * are routed to an isolated project instead; measured on `front-components`, 13 of 230.\n */\nimport { afterAll, afterEach, vi } from 'vitest'\n\nimport { CURRENT_FILE_MOCKS, PHASE, SHARED_WORKER } from './file-boundary-state.js'\nimport { alreadyInstalled } from './install-once.js'\nimport { fromModule } from './module-copy.js'\n\nconst state = globalThis as Record<symbol, unknown>\n\n// Anything whose lifecycle is the worker's rather than the file's reads this.\nstate[SHARED_WORKER] = true\n\n/* ── A document each file starts from ──────────────────────────────────────────────────────────\n *\n * Testing Library's cleanup removes the containers Testing Library rendered, and nothing else. A\n * test that writes `document.body.innerHTML` itself, or focuses an element, used to be forgiven by\n * the next file getting a new document.\n *\n * Reset at the START of the file rather than at its end, on purpose: a file that crashes never\n * runs its `afterAll`, and the next file should not inherit the wreckage of a file that failed.\n */\n// Reached through `globalThis` rather than the `document` global: sentinel's build emits types\n// without the DOM lib, and this same file is loaded by node-environment suites that have no\n// document at all.\nconst body = (globalThis as { document?: { body?: DocumentBody } }).document?.body\nif (body !== undefined) {\n body.innerHTML = ''\n body.removeAttribute('style')\n}\n\n/* ── Testing Library's cleanup, re-armed ───────────────────────────────────────────────────────\n *\n * `@testing-library/react` registers `afterEach(cleanup)` when it is imported, guarded by\n * `RTL_SKIP_AUTO_CLEANUP`. Without isolation that import happens once per worker, so the hook\n * belongs to whichever file pulled the module in first and every later file renders into a\n * document nobody clears. Measured: 342 of 1418 tests fail, almost all of them\n * `Found multiple elements`.\n *\n * Loaded defensively, the way `./workspace.js` loads `dotenv-flow`: a module that does not render\n * React has no reason to have Testing Library installed, and asking for it would make this file\n * unusable there.\n */\nconst testingLibrary = fromModule('@testing-library/react') as { cleanup?: () => void } | undefined\nif (typeof testingLibrary?.cleanup === 'function') afterEach(testingLibrary.cleanup)\n\n/* ── The mock register, given back its file ────────────────────────────────────────────────────\n *\n * Measured on plain Vitest 4, with no sentinel in the picture, against a\n * `vi.fn().mockImplementation(...)`:\n *\n * vi.clearAllMocks() implementation kept\n * vi.resetAllMocks() implementation DESTROYED\n * vi.restoreAllMocks() implementation kept\n *\n * Under jest that destruction could not outlive the file, because the next file re-evaluated the\n * module and rebuilt the mock. Without isolation it reaches every later file in the worker, so one\n * `vi.resetAllMocks()` in one hook leaves a shared fixture returning `undefined` for the rest of\n * the run. Bisected on `front-components`: `useDebounce.test.ts` does exactly that, and ten tests\n * in `Tabs.test.tsx` die of it.\n *\n * Two registers, therefore. What a setup file builds is infrastructure and is never reset; what\n * the test file builds is the file's own and behaves exactly as it always did. The phase is opened\n * here and closed by `./file-boundary-close.js`, last in `setupFiles`.\n */\nconst mocksInThisFile = new Set<MockLike>()\n\nstate[CURRENT_FILE_MOCKS] = mocksInThisFile\nstate[PHASE] = 'infrastructure'\n\nconst api = vi as unknown as MockApi\n\nif (!alreadyInstalled(vi, 'file-boundary')) {\n for (const factory of ['fn', 'spyOn'] as const) {\n const create = api[factory].bind(api)\n api[factory] = (...args: never[]) => {\n const mock = create(...args)\n register(mock)\n return mock\n }\n }\n\n for (const [name, method] of [\n ['clearAllMocks', 'mockClear'],\n ['resetAllMocks', 'mockReset'],\n ['restoreAllMocks', 'mockRestore'],\n ] as const) {\n api[name] = () => {\n for (const mock of currentFileMocks()) mock[method]?.()\n return api\n }\n }\n}\n\nafterAll(() => {\n /*\n * A spy patches something SHARED: a prototype, a global, another module's export. Isolation undid\n * that by throwing the registry away, so restoring at the boundary is the transposition, not an\n * improvement. Restoring the file's plain `vi.fn()` mocks too is harmless, since the file that\n * made them is over.\n */\n for (const mock of mocksInThisFile) mock.mockRestore?.()\n mocksInThisFile.clear()\n\n vi.useRealTimers()\n vi.unstubAllGlobals()\n vi.unstubAllEnvs()\n})\n\n/** Only what this file does to the document. The DOM lib is not in sentinel's build. */\ninterface DocumentBody {\n innerHTML: string\n removeAttribute: (name: string) => void\n}\n\n/** The part of `vi` this file replaces. Vitest's own overloaded types are not needed to say it. */\ninterface MockApi {\n fn: (...args: never[]) => MockLike\n spyOn: (...args: never[]) => MockLike\n clearAllMocks: () => unknown\n resetAllMocks: () => unknown\n restoreAllMocks: () => unknown\n}\n\n/** Only the part of a mock this file touches; Vitest's own generic types are not needed to say it. */\ninterface MockLike {\n mockClear?: () => unknown\n mockReset?: () => unknown\n mockRestore?: () => unknown\n}\n\n/**\n * Remembers a mock only while the TEST FILE is the one building them.\n *\n * ⚠️ An earlier version kept a second set for the mocks a setup file builds, so the global reset\n * APIs could skip them. It retained them instead: a setup file is re-executed per test file, so a\n * `vi.fn()` written at its module scope is a NEW mock every time, and `front-components` has\n * several (`matchMedia` among them). 230 files later the set held hundreds of mocks, each holding\n * its `mock.calls`, and through them whatever was passed in, DOM nodes included.\n *\n * Nothing ever read that set. Skipping the registration says the same thing and keeps nothing.\n */\nfunction register(mock: MockLike): void {\n if (state[PHASE] === 'infrastructure') return\n ;(state[CURRENT_FILE_MOCKS] as Set<MockLike> | undefined)?.add(mock)\n}\n\nfunction currentFileMocks(): Set<MockLike> {\n return (state[CURRENT_FILE_MOCKS] as Set<MockLike> | undefined) ?? new Set()\n}\n"],"mappings":";;;;;;;;;;;;;AAuDA,SAAS,UAAU,WAAW,UAAU;AAMxC,IAAM,QAAQ;AAGd,MAAM,aAAa,IAAI;AAcvB,IAAM,OAAQ,WAAsD,UAAU;AAC9E,IAAI,SAAS,QAAW;AACtB,OAAK,YAAY;AACjB,OAAK,gBAAgB,OAAO;AAC9B;AAcA,IAAM,iBAAiB,WAAW,wBAAwB;AAC1D,IAAI,OAAO,gBAAgB,YAAY,WAAY,WAAU,eAAe,OAAO;AAqBnF,IAAM,kBAAkB,oBAAI,IAAc;AAE1C,MAAM,kBAAkB,IAAI;AAC5B,MAAM,KAAK,IAAI;AAEf,IAAM,MAAM;AAEZ,IAAI,CAAC,iBAAiB,IAAI,eAAe,GAAG;AAC1C,aAAW,WAAW,CAAC,MAAM,OAAO,GAAY;AAC9C,UAAM,SAAS,IAAI,OAAO,EAAE,KAAK,GAAG;AACpC,QAAI,OAAO,IAAI,IAAI,SAAkB;AACnC,YAAM,OAAO,OAAO,GAAG,IAAI;AAC3B,eAAS,IAAI;AACb,aAAO;AAAA,IACT;AAAA,EACF;AAEA,aAAW,CAAC,MAAM,MAAM,KAAK;AAAA,IAC3B,CAAC,iBAAiB,WAAW;AAAA,IAC7B,CAAC,iBAAiB,WAAW;AAAA,IAC7B,CAAC,mBAAmB,aAAa;AAAA,EACnC,GAAY;AACV,QAAI,IAAI,IAAI,MAAM;AAChB,iBAAW,QAAQ,iBAAiB,EAAG,MAAK,MAAM,IAAI;AACtD,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAEA,SAAS,MAAM;AAOb,aAAW,QAAQ,gBAAiB,MAAK,cAAc;AACvD,kBAAgB,MAAM;AAEtB,KAAG,cAAc;AACjB,KAAG,iBAAiB;AACpB,KAAG,cAAc;AACnB,CAAC;AAmCD,SAAS,SAAS,MAAsB;AACtC,MAAI,MAAM,KAAK,MAAM,iBAAkB;AACtC,EAAC,MAAM,kBAAkB,GAAiC,IAAI,IAAI;AACrE;AAEA,SAAS,mBAAkC;AACzC,SAAQ,MAAM,kBAAkB,KAAmC,oBAAI,IAAI;AAC7E;","names":[]}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
alreadyInstalled
|
|
3
|
+
} from "../../../chunk-NUOQXAYR.js";
|
|
1
4
|
import {
|
|
2
5
|
installWorkspaceSetup
|
|
3
|
-
} from "../../../chunk-
|
|
6
|
+
} from "../../../chunk-Z7L4FGKP.js";
|
|
7
|
+
import "../../../chunk-ELZHIN6E.js";
|
|
4
8
|
import {
|
|
5
9
|
clearDeepMocks,
|
|
6
10
|
resetDeepMocks
|
|
7
11
|
} from "../../../chunk-2XLX6PFR.js";
|
|
8
|
-
import "../../../chunk-
|
|
12
|
+
import "../../../chunk-KMKQDGI6.js";
|
|
9
13
|
|
|
10
14
|
// src/roles/test/setup/jest-parity.ts
|
|
11
15
|
import { expect, vi } from "vitest";
|
|
@@ -21,6 +25,7 @@ function asConstructable(implementation) {
|
|
|
21
25
|
};
|
|
22
26
|
}
|
|
23
27
|
function installJestConstructorSemantics(vi2) {
|
|
28
|
+
if (alreadyInstalled(vi2, "jest-constructor-semantics")) return;
|
|
24
29
|
const patch = (mock) => {
|
|
25
30
|
const { mockImplementation, mockImplementationOnce } = mock;
|
|
26
31
|
mock.mockImplementation = function(implementation) {
|
|
@@ -48,6 +53,7 @@ function installJestConstructorSemantics(vi2) {
|
|
|
48
53
|
|
|
49
54
|
// src/roles/test/setup/deep-mock-reset.ts
|
|
50
55
|
function installDeepMockReset(vi2) {
|
|
56
|
+
if (alreadyInstalled(vi2, "deep-mock-reset")) return;
|
|
51
57
|
const { resetAllMocks, clearAllMocks } = vi2;
|
|
52
58
|
vi2.resetAllMocks = function extended() {
|
|
53
59
|
const answer = resetAllMocks.call(this);
|
|
@@ -67,6 +73,7 @@ function errorsCompareByMessage(left, right) {
|
|
|
67
73
|
return void 0;
|
|
68
74
|
}
|
|
69
75
|
function installJestErrorEquality(expect2) {
|
|
76
|
+
if (alreadyInstalled(expect2, "jest-error-equality")) return;
|
|
70
77
|
expect2.addEqualityTesters([errorsCompareByMessage]);
|
|
71
78
|
}
|
|
72
79
|
|
|
@@ -90,6 +97,7 @@ function withoutJestOnlyOptions(options) {
|
|
|
90
97
|
return { ...rest, toFake: base.filter((timer) => !doNotFake.includes(timer)) };
|
|
91
98
|
}
|
|
92
99
|
function installJestFakeTimerOptions(vi2) {
|
|
100
|
+
if (alreadyInstalled(vi2, "jest-fake-timer-options")) return;
|
|
93
101
|
const inherited = vi2.useFakeTimers.bind(vi2);
|
|
94
102
|
vi2.useFakeTimers = (options) => inherited(withoutJestOnlyOptions(options));
|
|
95
103
|
}
|
|
@@ -115,6 +123,7 @@ function resetLikeJest(mock) {
|
|
|
115
123
|
return mock;
|
|
116
124
|
}
|
|
117
125
|
function installJestMockReset(vi2) {
|
|
126
|
+
if (alreadyInstalled(vi2, "jest-mock-reset")) return;
|
|
118
127
|
for (const name of ["fn", "spyOn"]) {
|
|
119
128
|
const factory = vi2[name].bind(vi2);
|
|
120
129
|
vi2[name] = ((...args) => resetLikeJest(factory(...args)));
|
|
@@ -147,6 +156,13 @@ function installJestRejectedFunction() {
|
|
|
147
156
|
}
|
|
148
157
|
}
|
|
149
158
|
|
|
159
|
+
// src/roles/test/setup/worker-id.ts
|
|
160
|
+
function installWorkerId(env) {
|
|
161
|
+
if (env.JEST_WORKER_ID !== void 0) return;
|
|
162
|
+
if (env.VITEST_POOL_ID === void 0) return;
|
|
163
|
+
env.JEST_WORKER_ID = env.VITEST_POOL_ID;
|
|
164
|
+
}
|
|
165
|
+
|
|
150
166
|
// src/roles/test/setup/jest-parity.ts
|
|
151
167
|
installJestErrorEquality(expect);
|
|
152
168
|
installJestMockReset(vi);
|
|
@@ -155,5 +171,6 @@ installDeepMockReset(vi);
|
|
|
155
171
|
installJestFakeTimerOptions(vi);
|
|
156
172
|
installJestRejectedFunction();
|
|
157
173
|
installJestGlobal(vi);
|
|
174
|
+
installWorkerId(process.env);
|
|
158
175
|
await installWorkspaceSetup();
|
|
159
176
|
//# sourceMappingURL=jest-parity.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/roles/test/setup/jest-parity.ts","../../../../src/roles/test/setup/constructor-semantics.ts","../../../../src/roles/test/setup/deep-mock-reset.ts","../../../../src/roles/test/setup/error-equality.ts","../../../../src/roles/test/setup/fake-timers.ts","../../../../src/roles/test/setup/jest-global.ts","../../../../src/roles/test/setup/mock-reset.ts","../../../../src/roles/test/setup/rejected-function.ts"],"sourcesContent":["/**\n * The setup any migrated suite runs before its tests, as ONE entry point.\n *\n * Referenced by name from a generated config, never by a relative path:\n * `setupFiles: ['@hublo/sentinel/test/setup/jest-parity']`. Measured: Vitest resolves a bare\n * package specifier there, so nothing has to know where sentinel sits relative to the module.\n *\n * ⚠️ This was exported as `test/setup/nest` until 23/09, which was wrong in the one place it is\n * read. Nothing below is Nest-specific: every line restores a `jest.*` semantic under Vitest, plus\n * the `.env` cascade. But the name is COMMITTED into each adopting module's config, so a React\n * module ended up with `setupFiles: ['@hublo/sentinel/test/setup/nest', './jest.setup.js']` sitting\n * in its repository, which reads like the bug where a module was handed the wrong family's preset.\n * A reviewer cannot tell that apart from the real thing without opening our source. The name says\n * what the file does instead: parity with jest.\n *\n * ## What it replaces, line for line\n *\n * The repo's root `jest.setup.after.env.js`, loaded by 99 of the 111 jest configs:\n *\n * require('dotenv-flow').config({ silent: true, purge_dotenv: true })\n * const { Settings } = require('luxon')\n * const { server } = require('./libs/nest/tests/src/msw/server')\n * jest.mock('dynamoose')\n * jest.mock('@opentelemetry/exporter-metrics-otlp-grpc')\n * beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))\n * afterEach(() => { if (global.gc) global.gc() })\n * afterAll(() => server.close())\n * Settings.defaultZone = 'utc'\n *\n * Everything here is a transcription of that, not an improvement on it. Where the original made a\n * choice that looks questionable, the choice is carried across and reported, because a migration\n * that also changes behaviour cannot be verified against its own baseline.\n *\n * ONE line is not transcribed: `server.listen()` also runs when this file is evaluated, not only\n * inside `beforeAll`. The runners differ on when a module's exports can still be patched, so the\n * original placement silently disabled interception under Vitest and let tests reach the real\n * internet. `./msw.js` carries the measurement.\n *\n * ## Two halves, and why one of them is loaded defensively\n *\n * The msw lifecycle used to live here and now has its own file, `./msw-lifecycle.js`, added to a\n * module's `setupFiles` only when that module actually uses msw. Installing it everywhere patches\n * `http`/`https` and refuses unmatched requests for modules that never asked: measured on\n * `libs/nest/starter`, 11 of 11 became 10 of 11 with `TypeError: Invalid URL` in the interceptor,\n * on a test fetching the Fastify server the suite starts itself.\n *\n * `dotenv-flow` and luxon's zone react to the REPO, not to the runner: they would read the same\n * under jest, Vitest or `node:test`. So `./workspace.js` loads them only if they are installed and\n * skips them in silence otherwise. That defensiveness is not a precaution bolted on, it IS the\n * statement that these belong to whoever installed them. Elsewhere, sentinel installs and that half\n * does nothing.\n *\n * `dotenv-flow` cannot simply be dropped in favour of Vite's own `.env` handling, which was the\n * first thing checked. Measured on Vitest 4: it does read the cascade and sets `MODE=test`, but it\n * exposes only `VITE_`-prefixed values, and only on `import.meta.env`. `process.env` is left\n * untouched, and Nest code reads unprefixed `process.env`.\n *\n * ## What is NOT here\n *\n * The two `jest.mock` calls. A module mock is a per-suite decision that `vi.mock` must make in the\n * file that needs it; hoisting it into a shared setup is what makes a test pass for a reason nobody\n * can see. The migration reports them so the module relying on one declares it.\n *\n * And `Settings.defaultZone = 'utc'` is carried as luxon's own setting, never translated to\n * `process.env.TZ`. One configures luxon, the other the whole process, `Date` and `Intl` included.\n * Swapping them would change what the suite does while claiming to migrate it, and timezone is not\n * a detail: measured on `host-admin`, a machine's zone accounted for a large part of 345 local\n * failures that did not exist in CI.\n */\nimport { expect, vi } from 'vitest'\n\nimport { installJestConstructorSemantics } from './constructor-semantics.js'\nimport { installDeepMockReset } from './deep-mock-reset.js'\nimport { installJestErrorEquality } from './error-equality.js'\nimport { installJestFakeTimerOptions } from './fake-timers.js'\nimport { installJestGlobal } from './jest-global.js'\nimport { installJestMockReset } from './mock-reset.js'\nimport { installJestRejectedFunction } from './rejected-function.js'\nimport { installWorkspaceSetup } from './workspace.js'\n\ninstallJestErrorEquality(expect)\ninstallJestMockReset(vi)\ninstallJestConstructorSemantics(vi)\ninstallDeepMockReset(vi)\ninstallJestFakeTimerOptions(vi)\ninstallJestRejectedFunction()\ninstallJestGlobal(vi)\n\n/*\n * The `.env` cascade, unconditionally: a suite reading `process.env` needs it whatever setup its\n * jest config named, because Vitest's workers do not inherit what a `globalSetup` set in the main\n * process. Awaited at the top level, so it is loaded before the first test file is imported: a\n * module read at import time would already have captured an unset variable.\n *\n * ⚠️ What is NOT here any more is luxon's UTC zone. That was a decision ONE file at the workspace\n * root made, and only the 97 modules naming it ever had it; it lives in\n * `@hublo/sentinel/test/setup/workspace`, which the generated config adds only for those.\n */\nawait installWorkspaceSetup()\n","/**\n * `new` on a mock, the way jest answered it.\n *\n * ## The divergence, measured on the same source under both runners\n *\n * jest vitest\n * mockImplementation(() => obj), then new the obj TypeError: not a constructor\n * mockImplementation(function(){}), new works works\n * mockReturnValue(obj), then new the obj TypeError, with an explanation\n *\n * jest calls the implementation as a PLAIN function when the mock is constructed and hands back\n * what it returned. Vitest applies real `new` semantics, and an arrow function has no [[Construct]]\n * slot, so it throws.\n *\n * The pattern this breaks is the ordinary way to stand in for a class: automock the module, then\n * say what `new` should give back.\n *\n * jest.mock('@hublo/cloud/event-scheduler-sdk')\n * ;(EventSchedulerSDK as jest.MockedClass<typeof EventSchedulerSDK>)\n * .mockImplementation(() => mockEventSchedulerSDK)\n *\n * Measured across the nest and cloud campaigns: 2 modules, 15 tests.\n * `apps/nest/microservices/client-management` (9, an SDK) and `apps/nest/microservices/hublo-pool`\n * (6, a mocked `Date`).\n *\n * ## Why this is safe to do for everybody, which is the part that matters\n *\n * It only changes a case that THROWS today. An implementation without a `prototype` cannot be\n * constructed at all under Vitest, so no suite anywhere can be relying on what it does: the only\n * behaviours available are \"throws\" and \"answers like jest\". Nothing that works today changes\n * shape, and a constructable implementation is passed through untouched.\n *\n * That bound is asserted by the tests, not just claimed here.\n *\n * ## `mockReturnValue(obj)` then `new`, which this used to leave alone\n *\n * It was left out on the grounds that answering it would decide \"a return value and a constructed\n * instance are the same thing\", a claim about the suite. That reading was wrong on both halves.\n *\n * It is not a claim about the suite, because jest's answer is not ambiguous: it hands back the\n * value, exactly as it does for `mockImplementation(() => obj)`, which this file already restores.\n * Treating the two differently would be the arbitrary choice.\n *\n * And \"no module in this repo hit it\" stopped being true the moment the front family was measured.\n * `libs/front/components` mocks Google Maps the ordinary way and constructs it:\n *\n * AutocompleteService: jest.fn().mockReturnValue({ getPlacePredictions: jest.fn() })\n * // and the hook: new window.google.maps.places.AutocompleteService()\n *\n * 7 tests, all with the same TypeError. The safety bound is unchanged and it is the whole reason\n * this is allowed: Vitest THROWS on that call today, so no suite anywhere can depend on what it\n * does, and the only behaviours available are \"throws\" and \"answers like jest\".\n *\n * Expressed by routing the value through the implementation, rather than by a second mechanism:\n * `mockReturnValue(v)` IS `mockImplementation(() => v)`, so it goes through the same wrapper and\n * `new` works for the same reason.\n */\n\n/** The mocking surface this touches. Vitest's own type is not needed to say it. */\ninterface MockLike {\n mockImplementation(implementation: (...args: unknown[]) => unknown): unknown\n mockImplementationOnce(implementation: (...args: unknown[]) => unknown): unknown\n mockReturnValue(value: unknown): unknown\n mockReturnValueOnce(value: unknown): unknown\n}\n\ninterface ViLike {\n fn(implementation?: (...args: unknown[]) => unknown): MockLike\n /**\n * ⚠️ The REST, not `(target, key)`.\n *\n * Vitest's third argument is the access type, `'get'` or `'set'`, and the first version of the\n * wrapper below forwarded two arguments and dropped it. `vi.spyOn(el, 'scrollWidth', 'get')`\n * then became a spy on the VALUE of an accessor that only exists on a prototype, and jsdom\n * answered `'get scrollWidth' called on an object that is not a valid instance of Element`.\n *\n * Measured on `libs/front/components`, 2 tests, and invisible to every nest module because none\n * of them spies on a DOM accessor.\n */\n spyOn(target: object, ...rest: unknown[]): MockLike\n}\n\n/**\n * Can this function be used with `new`?\n *\n * Asked of the `prototype` property rather than of the source text: an arrow function, a shorthand\n * method and a bound function all lack it, and all three are exactly the cases that throw. A\n * class and a plain `function` have it.\n */\nfunction constructable(value: unknown): boolean {\n return (\n typeof value === 'function' && Object.getOwnPropertyDescriptor(value, 'prototype') !== undefined\n )\n}\n\n/**\n * The same implementation, reachable through `new`.\n *\n * A plain `function` that forwards the call and RETURNS the result. JavaScript's own `new` then\n * hands that object back, which is what jest did, so nothing here imitates jest by hand: it\n * restores the one property the arrow was missing and lets the language do the rest.\n */\nfunction asConstructable(\n implementation: (...args: unknown[]) => unknown,\n): (...args: unknown[]) => unknown {\n if (constructable(implementation)) return implementation\n\n return function forwarded(this: unknown, ...args: unknown[]): unknown {\n return implementation.apply(this, args)\n }\n}\n\n/**\n * Wrap `vi.fn` and `vi.spyOn` so every mock they produce accepts `new` the way jest's did.\n *\n * Wrapped at the factory, like `installJestMockReset`, because the behaviour belongs to every mock\n * a suite makes and a suite should not have to ask for it.\n */\nexport function installJestConstructorSemantics(vi: ViLike): void {\n const patch = (mock: MockLike): MockLike => {\n const { mockImplementation, mockImplementationOnce } = mock\n\n mock.mockImplementation = function (implementation) {\n return mockImplementation.call(this, asConstructable(implementation))\n }\n mock.mockImplementationOnce = function (implementation) {\n return mockImplementationOnce.call(this, asConstructable(implementation))\n }\n\n /*\n * Routed through the implementation rather than given a mechanism of its own: the two are the\n * same statement, and one of them already accepts `new`.\n */\n mock.mockReturnValue = function (value) {\n return this.mockImplementation(() => value)\n }\n mock.mockReturnValueOnce = function (value) {\n return this.mockImplementationOnce(() => value)\n }\n return mock\n }\n\n const { fn, spyOn } = vi\n\n vi.fn = function (implementation) {\n return patch(fn.call(this, implementation && asConstructable(implementation)))\n }\n vi.spyOn = function (target, ...rest) {\n return patch(spyOn.call(this, target, ...rest))\n }\n}\n","/**\n * `vi.resetAllMocks()` reaching the deep mocks, the way jest's registry did.\n *\n * ## The divergence, and why it is invisible\n *\n * jest built `jest-mock-extended`'s mocks with `jest.fn()`, so they sat in jest's own registry and\n * `jest.resetAllMocks()` cleared them with everything else. `vitest-mock-extended` builds them its\n * own way, so `vi.resetAllMocks()` walks past them and their call history survives into the next\n * test.\n *\n * Nothing announces it. The suite still runs, and an assertion fails several tests later with a\n * count that is off by exactly what its neighbour did.\n *\n * Measured on `apps/nest/microservices/activity`, whose suite does what jest expected:\n *\n * beforeEach(() => mocked.findEvents.mockResolvedValue([]))\n * afterEach(() => vi.resetAllMocks())\n *\n * Eleven tests asserting `toHaveBeenCalledTimes(0)` saw the call left by the one before them. Each\n * PASSES on its own and fails as soon as its neighbour runs first, which is the signature of\n * leakage rather than of a wrong assertion.\n *\n * ## What this installs, and what it leaves alone\n *\n * `resetAllMocks` and `clearAllMocks` do what they did, then extend to the deep mocks: `reset`\n * drops implementations as well as calls, `clear` drops only calls, which is the same distinction\n * the two names already carry.\n *\n * `restoreAllMocks` is NOT extended. It restores spies to their originals, and a deep mock has no\n * original to go back to: it was invented. Extending it would mean deciding what \"restore\" means\n * for something that never existed, which is a claim, not a translation.\n */\nimport { clearDeepMocks, resetDeepMocks } from './mock-extended.js'\n\n/** The part of `vi` this touches. Vitest's own type is not needed to say it. */\ninterface ViLike {\n resetAllMocks(): unknown\n clearAllMocks(): unknown\n}\n\nexport function installDeepMockReset(vi: ViLike): void {\n const { resetAllMocks, clearAllMocks } = vi\n\n vi.resetAllMocks = function extended(this: unknown): unknown {\n const answer = resetAllMocks.call(this)\n resetDeepMocks()\n return answer\n }\n\n vi.clearAllMocks = function extended(this: unknown): unknown {\n const answer = clearAllMocks.call(this)\n clearDeepMocks()\n return answer\n }\n}\n","/**\n * How two `Error` values compare, which the two runners disagree about.\n *\n * A suite that asserts on a thrown or captured error usually writes the error it expects by hand:\n *\n * expect(save).toHaveBeenCalledWith({ error: new AxiosError('Request failed with status code 500'), ... })\n *\n * Under jest that passes whatever else the real error carries. Under Vitest it fails, and the\n * report is 6600 lines of an axios error's `config`, `request` and `response`, which reads like a\n * broken test rather than a runner difference.\n *\n * ## What each runner actually does, measured on the same four cases\n *\n * | two errors | jest 29 | Vitest 4 |\n * | --------------------------------- | -------- | ----------- |\n * | same message, same type | equal | equal |\n * | same message, DIFFERENT types | equal | not equal |\n * | same message, extra properties | equal | not equal |\n * | different messages | not equal| not equal |\n *\n * jest compares errors by their MESSAGE and nothing else: a `TypeError` and a `RangeError` with the\n * same text are equal to it. Vitest compares the type and the own properties too.\n *\n * ## Why the looser rule is the one restored\n *\n * Because it is the one 3481 test files were written against. Tightening it here would turn green\n * tests red during a migration whose whole promise is that the suite means the same thing\n * afterwards, and a baseline gate cannot tell that kind of loss from a real one.\n *\n * The question is reported rather than settled: comparing the type as well would be a better rule,\n * and it may cost nothing on this corpus. That is a measurement to run and a change to make on its\n * own, once the suites no longer move. Measured need so far: `libs/cloud/shared`, whose last\n * missing test was exactly this.\n */\nimport type { expect as ExpectApi } from 'vitest'\n\n/**\n * Restore jest's rule: two errors are equal when their messages are.\n *\n * Returning `undefined` for anything else hands the pair back to the default comparison, which is\n * what an equality tester is expected to do for values it has no opinion about.\n */\nexport function errorsCompareByMessage(left: unknown, right: unknown): boolean | undefined {\n if (left instanceof Error && right instanceof Error) return left.message === right.message\n return undefined\n}\n\nexport function installJestErrorEquality(expect: typeof ExpectApi): void {\n expect.addEqualityTesters([errorsCompareByMessage])\n}\n","/**\n * `useFakeTimers({ doNotFake: [...] })`, which Vitest accepts and ignores.\n *\n * jest names what to LEAVE ALONE, Vitest names what to FAKE. The option Vitest does not know is\n * dropped in silence, so a suite that carefully kept `setTimeout` real gets it faked, and anything\n * awaiting a timer never resolves.\n *\n * Measured on both runners with the same source:\n *\n * useFakeTimers({ doNotFake: ['setTimeout'] }) jest: setTimeout real Vitest: setTimeout FAKED\n * useFakeTimers({ toFake: ['Date'] }) Vitest: setTimeout real\n *\n * Found on `libs/cloud/events-notifications`: 4 tests in one file died on `Test timed out in\n * 5000ms` with nothing else to show, because the code under test awaits a real timer. The repo has\n * 2 files using `doNotFake`, the other in `apps/nest/microservices/institution`.\n *\n * ## The translation, and what it inherits\n *\n * `doNotFake: [a, b]` becomes `toFake: <everything the runner fakes by default> minus [a, b]`. The\n * default set is Vitest's, measured rather than assumed, and NOT jest's, which is wider: jest also\n * fakes `nextTick`, `queueMicrotask` and the animation-frame pair. Subtracting from Vitest's own\n * default is what every other `useFakeTimers()` call in the corpus already gets, so this keeps one\n * behaviour for the whole migration instead of two.\n */\n\n/**\n * What `vi.useFakeTimers()` replaces when told nothing, measured on Vitest 4 by comparing each\n * global before and after the call.\n */\nconst FAKED_BY_DEFAULT = [\n 'setTimeout',\n 'clearTimeout',\n 'setInterval',\n 'clearInterval',\n 'setImmediate',\n 'clearImmediate',\n 'Date',\n 'performance',\n 'hrtime',\n] as const\n\n/** The options both runners take, plus the one only jest knows. */\ninterface TimerOptions {\n toFake?: string[]\n doNotFake?: string[]\n}\n\n/**\n * Turn \"leave these alone\" into \"fake those\", leaving anything else untouched.\n *\n * Exported for its own test: the translation is the whole rule, and asserting it directly says more\n * than asserting that a wrapper was installed.\n */\nexport function withoutJestOnlyOptions<T>(options: T): T {\n const given = options as TimerOptions | undefined\n if (given?.doNotFake === undefined) return options\n\n const { doNotFake, ...rest } = given\n const base = rest.toFake ?? [...FAKED_BY_DEFAULT]\n\n return { ...rest, toFake: base.filter((timer) => !doNotFake.includes(timer)) } as T\n}\n\n/**\n * The one function this touches, named by its shape rather than by Vitest's type.\n *\n * `Options` is the caller's own parameter type: the wrapper hands back exactly what it was given,\n * minus the option Vitest does not know, so it must not narrow what the runner accepts.\n */\ninterface FakeTimerApi<Options> {\n useFakeTimers: (options?: Options) => unknown\n}\n\n/** Wrap `vi.useFakeTimers` so a jest-shaped options object still means what it said. */\nexport function installJestFakeTimerOptions<Options>(vi: FakeTimerApi<Options>): void {\n const inherited = vi.useFakeTimers.bind(vi)\n vi.useFakeTimers = (options?: Options) => inherited(withoutJestOnlyOptions(options))\n}\n","/**\n * The `jest` global, kept alive for helpers that a migrating module is not allowed to edit.\n *\n * ## Why a module cannot solve this for itself\n *\n * The codemod rewrites a module's own test files. It does not rewrite files in OTHER projects, and\n * it must not: a shared helper is imported by modules still on jest, so migrating it would break\n * them, and leaving it breaks the migrated one. That is the constraint the whole per-module plan\n * rests on.\n *\n * But those helpers call the jest API at MODULE scope. `libs/front/tests/src/mocks/**` does\n * `jest.fn()` when it is imported, before any test runs, so a migrated module dies on\n * `ReferenceError: jest is not defined` the moment it imports one.\n *\n * Measured repo-wide, excluding documentation: **50 files use the jest API without being test\n * files**, in `jest.setup.js`, `*.mock.ts`, `*.test-helper.ts`, `*.test-wrapper.ts`. Three\n * independent hand migrations reached this same line without knowing about each other:\n * `libs/front/components` (8 shared helpers, 16 sites), `apps/nest/microservices/mission` and\n * `apps/nest/backends-for-frontends/admin`.\n *\n * ## It is `vi`, not a fake jest\n *\n * The global IS Vitest's `vi`, so anything Vitest does not have keeps failing loudly:\n * `jest.requireActual` and `jest.isolateModules` are still errors, and a module relying on them\n * still has to be migrated properly. Handing over a hand-written imitation would turn those into\n * silent wrong behaviour, which is the opposite of the point.\n *\n * ## ⚠️ What it does NOT cover, and this bound is measured\n *\n * `jest.mock()`. Vitest hoists mock registrations above the imports by scanning the source\n * STATICALLY, and that scan only recognises the receivers `vi` and `vitest` (`@vitest/mocker`,\n * `hoistMocksPlugin`). A `jest.mock()` left in place is therefore NOT hoisted: it runs after the\n * imports it was meant to intercept and does nothing at all, in silence. Measured on\n * `apps/front/front-legacy`, where 216 of 427 files call it.\n *\n * So this covers a helper that CALLS the jest API. It does not make an unmigrated test file work,\n * and the codemod's rename stays load-bearing rather than cosmetic.\n */\n\n/** The part of `vi` this installs. Vitest's own type is not needed to say it. */\ntype JestLike = object\n\n/**\n * Put `vi` on `globalThis` under the name `jest`.\n *\n * Assigned rather than defined with a getter: a helper may well write to it (`jest.fn = ...` in a\n * test double), and a getter-only property would throw where jest allowed it.\n */\nexport function installJestGlobal(vi: JestLike): void {\n ;(globalThis as Record<string, unknown>).jest = vi\n}\n","/**\n * What `mockReset()` leaves behind, which is where the two runners disagree most dangerously.\n *\n * jest REMOVES the implementation: a reset spy returns `undefined` and the real function is not\n * called. Vitest puts the ORIGINAL implementation back: a reset spy calls the real function again.\n *\n * Measured on both runners with the same source:\n *\n * after resetAllMocks() on a spy jest: undefined Vitest: the real function\n * after mockReset() on a spy jest: undefined Vitest: the real function\n * after mockReset() on fn(impl) jest: undefined Vitest: impl\n *\n * The shape this breaks is ordinary and common: a suite spies on a provider in `beforeAll` and\n * resets its mocks in `beforeEach`. Under jest the provider stayed neutralised for every test.\n * Under Vitest the first `beforeEach` hands the real provider back, and every test after it runs\n * the real code. Measured on `libs/cloud/events-notifications`, that meant real HTTP: 19 tests\n * failed on `captured a request without a matching request handler` for the hermes API and 23 more\n * timed out waiting on it. Nothing in any report named a reset.\n *\n * 240 files in this repo both spy and reset, in every family: 111 under `apps/nest`, 77 under\n * `libs/cloud`, 31 under `apps/front`.\n *\n * ## Why here and not in the 240 files\n *\n * Because a codemod would have to decide, per spy, whether the suite wanted the real function\n * back, and the answer is in the test's intent rather than in its text. The runner-level rule is\n * the one that was true for all 240 while they were written, so restoring it is the transcription\n * and rewriting them would be the guess.\n *\n * Vitest routes `vi.resetAllMocks()` through each mock's own `mockReset`, measured, so overriding\n * that method covers the bulk form as well as the direct one. `mockRestore` is untouched: it puts\n * the original back under both runners, which is what it is for.\n */\n\n/** The part of a mock this file touches. Vitest's own types are not needed to say it. */\ninterface ResettableMock {\n mockReset: () => unknown\n mockImplementation: (fn: (...args: unknown[]) => unknown) => unknown\n}\n\nfunction isResettable(value: unknown): value is ResettableMock {\n return (\n typeof value === 'function' &&\n typeof (value as Partial<ResettableMock>).mockReset === 'function' &&\n typeof (value as Partial<ResettableMock>).mockImplementation === 'function'\n )\n}\n\n/**\n * Make one mock forget its implementation on reset, as jest's did.\n *\n * The override is installed on the instance rather than on a prototype: mocks are functions with\n * their own properties, and there is no shared prototype to reach.\n */\nfunction resetLikeJest<T>(mock: T): T {\n if (!isResettable(mock)) return mock\n\n const inherited = mock.mockReset.bind(mock)\n mock.mockReset = () => {\n inherited()\n mock.mockImplementation(() => undefined)\n return mock\n }\n return mock\n}\n\n/** The two factories a suite gets its mocks from. */\ntype MockFactories = { fn: (...args: never[]) => unknown; spyOn: (...args: never[]) => unknown }\n\n/**\n * Wrap `vi.fn` and `vi.spyOn` so everything they hand out resets the way jest's did.\n *\n * Called with the `vi` a setup file imports, so nothing here reaches for a global.\n */\nexport function installJestMockReset(vi: MockFactories): void {\n for (const name of ['fn', 'spyOn'] as const) {\n const factory = vi[name].bind(vi) as (...args: never[]) => unknown\n vi[name] = ((...args: never[]) => resetLikeJest(factory(...args))) as MockFactories[typeof name]\n }\n}\n","/**\n * A rejected value that is a FUNCTION, which jest calls and Vitest does not.\n *\n * ## The divergence, measured rather than reasoned\n *\n * jest's `toThrow` decides what was thrown like this: under `.rejects` it uses the rejection\n * reason ONLY when that reason is an Error. Otherwise it falls through to the ordinary branch,\n * sees a function, and CALLS it, asserting on whatever that call throws.\n *\n * Probed on this repo under jest 29, with controls:\n *\n * reject(() => { throw new Error('some error') })\n * await expect(…).rejects.toThrow(new Error('some error')) -> passes\n * await expect(…).rejects.toThrow(new Error('other text')) -> fails\n * await expect(…).rejects.toThrow('other text') -> fails\n *\n * The two controls are what make the first line mean something: jest is not passing everything,\n * it really is comparing the message of the error the CALL produced.\n *\n * Vitest treats the rejection reason as the thrown value, so the assertion is made against a\n * function. A function has no `message`, and the failure reads\n * `Cannot read properties of undefined (reading 'indexOf')`, which names nothing near the cause.\n *\n * ## Where it shows, and what it is worth\n *\n * Measured across the nest campaign: 4 tests in 3 modules.\n * `apps/nest/microservices/worker` (1), `apps/nest/microservices/institution` (2) and\n * `apps/nest/microservices/mission` (1). All four write the same shape, a mock rejecting with a\n * thunk that throws, or a `throw <a function>`.\n *\n * ## What this changes, and what it cannot\n *\n * Only a case that CANNOT work today: under `.rejects`, a reason that is a function and not an\n * Error. Vitest has no useful behaviour there, so nothing that passes today changes shape. An\n * Error reason, a string, an object, a rejected value of any other kind, and every assertion\n * outside `.rejects` all reach Vitest's own matcher untouched.\n *\n * ⚠️ It is jest's behaviour, not a good one. A suite reaching it is asserting on a function it\n * never meant to hand over, and it passed by accident of the runner. Reproducing it is what keeps\n * the migration honest: the gate promises the suite means the same thing afterwards, and a test\n * that was green cannot be turned red by us and called a finding. The teams own the cleanup.\n */\nimport { chai } from 'vitest'\n\n/** The part of a chai assertion this touches. Chai's own types are not needed to say it. */\ninterface AssertionLike {\n _obj: unknown\n}\n\ntype Matcher = (this: AssertionLike, ...args: unknown[]) => unknown\n\n/**\n * What calling the function throws, or the function itself when it throws nothing.\n *\n * Returning it unchanged matters: a function that completes is not \"nothing was thrown\", and\n * handing Vitest the same value it had leaves the report exactly as it would have been.\n */\nfunction thrownByCalling(candidate: () => unknown): unknown {\n try {\n candidate()\n } catch (thrown) {\n return thrown\n }\n return candidate\n}\n\nexport function installJestRejectedFunction(): void {\n const { Assertion, util } = chai as unknown as {\n Assertion: { prototype: Record<string, unknown> }\n util: { flag(object: unknown, key: string): unknown }\n }\n\n for (const name of ['toThrow', 'toThrowError']) {\n const original = Assertion.prototype[name] as Matcher | undefined\n if (typeof original !== 'function') continue\n\n Assertion.prototype[name] = function patched(this: AssertionLike, ...args: unknown[]): unknown {\n const reason = this._obj\n const rejected = util.flag(this, 'promise') === 'rejects'\n\n if (rejected && typeof reason === 'function' && !(reason instanceof Error)) {\n this._obj = thrownByCalling(reason as () => unknown)\n }\n\n return original.apply(this, args)\n } as unknown as Matcher\n }\n}\n"],"mappings":"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","names":["vi","vi","expect","vi","vi","vi"]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/roles/test/setup/jest-parity.ts","../../../../src/roles/test/setup/constructor-semantics.ts","../../../../src/roles/test/setup/deep-mock-reset.ts","../../../../src/roles/test/setup/error-equality.ts","../../../../src/roles/test/setup/fake-timers.ts","../../../../src/roles/test/setup/jest-global.ts","../../../../src/roles/test/setup/mock-reset.ts","../../../../src/roles/test/setup/rejected-function.ts","../../../../src/roles/test/setup/worker-id.ts"],"sourcesContent":["/**\n * The setup any migrated suite runs before its tests, as ONE entry point.\n *\n * Referenced by name from a generated config, never by a relative path:\n * `setupFiles: ['@hublo/sentinel/test/setup/jest-parity']`. Measured: Vitest resolves a bare\n * package specifier there, so nothing has to know where sentinel sits relative to the module.\n *\n * ⚠️ This was exported as `test/setup/nest` until 23/09, which was wrong in the one place it is\n * read. Nothing below is Nest-specific: every line restores a `jest.*` semantic under Vitest, plus\n * the `.env` cascade. But the name is COMMITTED into each adopting module's config, so a React\n * module ended up with `setupFiles: ['@hublo/sentinel/test/setup/nest', './jest.setup.js']` sitting\n * in its repository, which reads like the bug where a module was handed the wrong family's preset.\n * A reviewer cannot tell that apart from the real thing without opening our source. The name says\n * what the file does instead: parity with jest.\n *\n * ## What it replaces, line for line\n *\n * The repo's root `jest.setup.after.env.js`, loaded by 99 of the 111 jest configs:\n *\n * require('dotenv-flow').config({ silent: true, purge_dotenv: true })\n * const { Settings } = require('luxon')\n * const { server } = require('./libs/nest/tests/src/msw/server')\n * jest.mock('dynamoose')\n * jest.mock('@opentelemetry/exporter-metrics-otlp-grpc')\n * beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))\n * afterEach(() => { if (global.gc) global.gc() })\n * afterAll(() => server.close())\n * Settings.defaultZone = 'utc'\n *\n * Everything here is a transcription of that, not an improvement on it. Where the original made a\n * choice that looks questionable, the choice is carried across and reported, because a migration\n * that also changes behaviour cannot be verified against its own baseline.\n *\n * ONE line is not transcribed: `server.listen()` also runs when this file is evaluated, not only\n * inside `beforeAll`. The runners differ on when a module's exports can still be patched, so the\n * original placement silently disabled interception under Vitest and let tests reach the real\n * internet. `./msw.js` carries the measurement.\n *\n * ## Two halves, and why one of them is loaded defensively\n *\n * The msw lifecycle used to live here and now has its own file, `./msw-lifecycle.js`, added to a\n * module's `setupFiles` only when that module actually uses msw. Installing it everywhere patches\n * `http`/`https` and refuses unmatched requests for modules that never asked: measured on\n * `libs/nest/starter`, 11 of 11 became 10 of 11 with `TypeError: Invalid URL` in the interceptor,\n * on a test fetching the Fastify server the suite starts itself.\n *\n * `dotenv-flow` and luxon's zone react to the REPO, not to the runner: they would read the same\n * under jest, Vitest or `node:test`. So `./workspace.js` loads them only if they are installed and\n * skips them in silence otherwise. That defensiveness is not a precaution bolted on, it IS the\n * statement that these belong to whoever installed them. Elsewhere, sentinel installs and that half\n * does nothing.\n *\n * `dotenv-flow` cannot simply be dropped in favour of Vite's own `.env` handling, which was the\n * first thing checked. Measured on Vitest 4: it does read the cascade and sets `MODE=test`, but it\n * exposes only `VITE_`-prefixed values, and only on `import.meta.env`. `process.env` is left\n * untouched, and Nest code reads unprefixed `process.env`.\n *\n * ## What is NOT here\n *\n * The two `jest.mock` calls. A module mock is a per-suite decision that `vi.mock` must make in the\n * file that needs it; hoisting it into a shared setup is what makes a test pass for a reason nobody\n * can see. The migration reports them so the module relying on one declares it.\n *\n * And `Settings.defaultZone = 'utc'` is carried as luxon's own setting, never translated to\n * `process.env.TZ`. One configures luxon, the other the whole process, `Date` and `Intl` included.\n * Swapping them would change what the suite does while claiming to migrate it, and timezone is not\n * a detail: measured on `host-admin`, a machine's zone accounted for a large part of 345 local\n * failures that did not exist in CI.\n */\nimport { expect, vi } from 'vitest'\n\nimport { installJestConstructorSemantics } from './constructor-semantics.js'\nimport { installDeepMockReset } from './deep-mock-reset.js'\nimport { installJestErrorEquality } from './error-equality.js'\nimport { installJestFakeTimerOptions } from './fake-timers.js'\nimport { installJestGlobal } from './jest-global.js'\nimport { installJestMockReset } from './mock-reset.js'\nimport { installJestRejectedFunction } from './rejected-function.js'\nimport { installWorkerId } from './worker-id.js'\nimport { installWorkspaceSetup } from './workspace.js'\n\ninstallJestErrorEquality(expect)\ninstallJestMockReset(vi)\ninstallJestConstructorSemantics(vi)\ninstallDeepMockReset(vi)\ninstallJestFakeTimerOptions(vi)\ninstallJestRejectedFunction()\ninstallJestGlobal(vi)\ninstallWorkerId(process.env)\n\n/*\n * The `.env` cascade, unconditionally: a suite reading `process.env` needs it whatever setup its\n * jest config named, because Vitest's workers do not inherit what a `globalSetup` set in the main\n * process. Awaited at the top level, so it is loaded before the first test file is imported: a\n * module read at import time would already have captured an unset variable.\n *\n * ⚠️ What is NOT here any more is luxon's UTC zone. That was a decision ONE file at the workspace\n * root made, and only the 97 modules naming it ever had it; it lives in\n * `@hublo/sentinel/test/setup/workspace`, which the generated config adds only for those.\n */\nawait installWorkspaceSetup()\n","/**\n * `new` on a mock, the way jest answered it.\n *\n * ## The divergence, measured on the same source under both runners\n *\n * jest vitest\n * mockImplementation(() => obj), then new the obj TypeError: not a constructor\n * mockImplementation(function(){}), new works works\n * mockReturnValue(obj), then new the obj TypeError, with an explanation\n *\n * jest calls the implementation as a PLAIN function when the mock is constructed and hands back\n * what it returned. Vitest applies real `new` semantics, and an arrow function has no [[Construct]]\n * slot, so it throws.\n *\n * The pattern this breaks is the ordinary way to stand in for a class: automock the module, then\n * say what `new` should give back.\n *\n * jest.mock('@hublo/cloud/event-scheduler-sdk')\n * ;(EventSchedulerSDK as jest.MockedClass<typeof EventSchedulerSDK>)\n * .mockImplementation(() => mockEventSchedulerSDK)\n *\n * Measured across the nest and cloud campaigns: 2 modules, 15 tests.\n * `apps/nest/microservices/client-management` (9, an SDK) and `apps/nest/microservices/hublo-pool`\n * (6, a mocked `Date`).\n *\n * ## Why this is safe to do for everybody, which is the part that matters\n *\n * It only changes a case that THROWS today. An implementation without a `prototype` cannot be\n * constructed at all under Vitest, so no suite anywhere can be relying on what it does: the only\n * behaviours available are \"throws\" and \"answers like jest\". Nothing that works today changes\n * shape, and a constructable implementation is passed through untouched.\n *\n * That bound is asserted by the tests, not just claimed here.\n *\n * ## `mockReturnValue(obj)` then `new`, which this used to leave alone\n *\n * It was left out on the grounds that answering it would decide \"a return value and a constructed\n * instance are the same thing\", a claim about the suite. That reading was wrong on both halves.\n *\n * It is not a claim about the suite, because jest's answer is not ambiguous: it hands back the\n * value, exactly as it does for `mockImplementation(() => obj)`, which this file already restores.\n * Treating the two differently would be the arbitrary choice.\n *\n * And \"no module in this repo hit it\" stopped being true the moment the front family was measured.\n * `libs/front/components` mocks Google Maps the ordinary way and constructs it:\n *\n * AutocompleteService: jest.fn().mockReturnValue({ getPlacePredictions: jest.fn() })\n * // and the hook: new window.google.maps.places.AutocompleteService()\n *\n * 7 tests, all with the same TypeError. The safety bound is unchanged and it is the whole reason\n * this is allowed: Vitest THROWS on that call today, so no suite anywhere can depend on what it\n * does, and the only behaviours available are \"throws\" and \"answers like jest\".\n *\n * Expressed by routing the value through the implementation, rather than by a second mechanism:\n * `mockReturnValue(v)` IS `mockImplementation(() => v)`, so it goes through the same wrapper and\n * `new` works for the same reason.\n */\n\nimport { alreadyInstalled } from './install-once.js'\n\n/** The mocking surface this touches. Vitest's own type is not needed to say it. */\ninterface MockLike {\n mockImplementation(implementation: (...args: unknown[]) => unknown): unknown\n mockImplementationOnce(implementation: (...args: unknown[]) => unknown): unknown\n mockReturnValue(value: unknown): unknown\n mockReturnValueOnce(value: unknown): unknown\n}\n\ninterface ViLike {\n fn(implementation?: (...args: unknown[]) => unknown): MockLike\n /**\n * ⚠️ The REST, not `(target, key)`.\n *\n * Vitest's third argument is the access type, `'get'` or `'set'`, and the first version of the\n * wrapper below forwarded two arguments and dropped it. `vi.spyOn(el, 'scrollWidth', 'get')`\n * then became a spy on the VALUE of an accessor that only exists on a prototype, and jsdom\n * answered `'get scrollWidth' called on an object that is not a valid instance of Element`.\n *\n * Measured on `libs/front/components`, 2 tests, and invisible to every nest module because none\n * of them spies on a DOM accessor.\n */\n spyOn(target: object, ...rest: unknown[]): MockLike\n}\n\n/**\n * Can this function be used with `new`?\n *\n * Asked of the `prototype` property rather than of the source text: an arrow function, a shorthand\n * method and a bound function all lack it, and all three are exactly the cases that throw. A\n * class and a plain `function` have it.\n */\nfunction constructable(value: unknown): boolean {\n return (\n typeof value === 'function' && Object.getOwnPropertyDescriptor(value, 'prototype') !== undefined\n )\n}\n\n/**\n * The same implementation, reachable through `new`.\n *\n * A plain `function` that forwards the call and RETURNS the result. JavaScript's own `new` then\n * hands that object back, which is what jest did, so nothing here imitates jest by hand: it\n * restores the one property the arrow was missing and lets the language do the rest.\n */\nfunction asConstructable(\n implementation: (...args: unknown[]) => unknown,\n): (...args: unknown[]) => unknown {\n if (constructable(implementation)) return implementation\n\n return function forwarded(this: unknown, ...args: unknown[]): unknown {\n return implementation.apply(this, args)\n }\n}\n\n/**\n * Wrap `vi.fn` and `vi.spyOn` so every mock they produce accepts `new` the way jest's did.\n *\n * Wrapped at the factory, like `installJestMockReset`, because the behaviour belongs to every mock\n * a suite makes and a suite should not have to ask for it.\n */\nexport function installJestConstructorSemantics(vi: ViLike): void {\n // A setup file runs once per TEST FILE, and without isolation `vi` is the worker's\n // single object, so a second pass would wrap the first pass's wrapper.\n if (alreadyInstalled(vi, 'jest-constructor-semantics')) return\n const patch = (mock: MockLike): MockLike => {\n const { mockImplementation, mockImplementationOnce } = mock\n\n mock.mockImplementation = function (implementation) {\n return mockImplementation.call(this, asConstructable(implementation))\n }\n mock.mockImplementationOnce = function (implementation) {\n return mockImplementationOnce.call(this, asConstructable(implementation))\n }\n\n /*\n * Routed through the implementation rather than given a mechanism of its own: the two are the\n * same statement, and one of them already accepts `new`.\n */\n mock.mockReturnValue = function (value) {\n return this.mockImplementation(() => value)\n }\n mock.mockReturnValueOnce = function (value) {\n return this.mockImplementationOnce(() => value)\n }\n return mock\n }\n\n const { fn, spyOn } = vi\n\n vi.fn = function (implementation) {\n return patch(fn.call(this, implementation && asConstructable(implementation)))\n }\n vi.spyOn = function (target, ...rest) {\n return patch(spyOn.call(this, target, ...rest))\n }\n}\n","import { alreadyInstalled } from './install-once.js'\n/**\n * `vi.resetAllMocks()` reaching the deep mocks, the way jest's registry did.\n *\n * ## The divergence, and why it is invisible\n *\n * jest built `jest-mock-extended`'s mocks with `jest.fn()`, so they sat in jest's own registry and\n * `jest.resetAllMocks()` cleared them with everything else. `vitest-mock-extended` builds them its\n * own way, so `vi.resetAllMocks()` walks past them and their call history survives into the next\n * test.\n *\n * Nothing announces it. The suite still runs, and an assertion fails several tests later with a\n * count that is off by exactly what its neighbour did.\n *\n * Measured on `apps/nest/microservices/activity`, whose suite does what jest expected:\n *\n * beforeEach(() => mocked.findEvents.mockResolvedValue([]))\n * afterEach(() => vi.resetAllMocks())\n *\n * Eleven tests asserting `toHaveBeenCalledTimes(0)` saw the call left by the one before them. Each\n * PASSES on its own and fails as soon as its neighbour runs first, which is the signature of\n * leakage rather than of a wrong assertion.\n *\n * ## What this installs, and what it leaves alone\n *\n * `resetAllMocks` and `clearAllMocks` do what they did, then extend to the deep mocks: `reset`\n * drops implementations as well as calls, `clear` drops only calls, which is the same distinction\n * the two names already carry.\n *\n * `restoreAllMocks` is NOT extended. It restores spies to their originals, and a deep mock has no\n * original to go back to: it was invented. Extending it would mean deciding what \"restore\" means\n * for something that never existed, which is a claim, not a translation.\n */\nimport { clearDeepMocks, resetDeepMocks } from './mock-extended.js'\n\n/** The part of `vi` this touches. Vitest's own type is not needed to say it. */\ninterface ViLike {\n resetAllMocks(): unknown\n clearAllMocks(): unknown\n}\n\nexport function installDeepMockReset(vi: ViLike): void {\n // A setup file runs once per TEST FILE, and without isolation `vi` is the worker's\n // single object, so a second pass would wrap the first pass's wrapper.\n if (alreadyInstalled(vi, 'deep-mock-reset')) return\n const { resetAllMocks, clearAllMocks } = vi\n\n vi.resetAllMocks = function extended(this: unknown): unknown {\n const answer = resetAllMocks.call(this)\n resetDeepMocks()\n return answer\n }\n\n vi.clearAllMocks = function extended(this: unknown): unknown {\n const answer = clearAllMocks.call(this)\n clearDeepMocks()\n return answer\n }\n}\n","/**\n * How two `Error` values compare, which the two runners disagree about.\n *\n * A suite that asserts on a thrown or captured error usually writes the error it expects by hand:\n *\n * expect(save).toHaveBeenCalledWith({ error: new AxiosError('Request failed with status code 500'), ... })\n *\n * Under jest that passes whatever else the real error carries. Under Vitest it fails, and the\n * report is 6600 lines of an axios error's `config`, `request` and `response`, which reads like a\n * broken test rather than a runner difference.\n *\n * ## What each runner actually does, measured on the same four cases\n *\n * | two errors | jest 29 | Vitest 4 |\n * | --------------------------------- | -------- | ----------- |\n * | same message, same type | equal | equal |\n * | same message, DIFFERENT types | equal | not equal |\n * | same message, extra properties | equal | not equal |\n * | different messages | not equal| not equal |\n *\n * jest compares errors by their MESSAGE and nothing else: a `TypeError` and a `RangeError` with the\n * same text are equal to it. Vitest compares the type and the own properties too.\n *\n * ## Why the looser rule is the one restored\n *\n * Because it is the one 3481 test files were written against. Tightening it here would turn green\n * tests red during a migration whose whole promise is that the suite means the same thing\n * afterwards, and a baseline gate cannot tell that kind of loss from a real one.\n *\n * The question is reported rather than settled: comparing the type as well would be a better rule,\n * and it may cost nothing on this corpus. That is a measurement to run and a change to make on its\n * own, once the suites no longer move. Measured need so far: `libs/cloud/shared`, whose last\n * missing test was exactly this.\n */\nimport type { expect as ExpectApi } from 'vitest'\n\nimport { alreadyInstalled } from './install-once.js'\n\n/**\n * Restore jest's rule: two errors are equal when their messages are.\n *\n * Returning `undefined` for anything else hands the pair back to the default comparison, which is\n * what an equality tester is expected to do for values it has no opinion about.\n */\nexport function errorsCompareByMessage(left: unknown, right: unknown): boolean | undefined {\n if (left instanceof Error && right instanceof Error) return left.message === right.message\n return undefined\n}\n\nexport function installJestErrorEquality(expect: typeof ExpectApi): void {\n // A setup file runs once per TEST FILE, and without isolation `expect` is the worker's\n // single object, so a second pass would wrap the first pass's wrapper.\n if (alreadyInstalled(expect, 'jest-error-equality')) return\n expect.addEqualityTesters([errorsCompareByMessage])\n}\n","/**\n * `useFakeTimers({ doNotFake: [...] })`, which Vitest accepts and ignores.\n *\n * jest names what to LEAVE ALONE, Vitest names what to FAKE. The option Vitest does not know is\n * dropped in silence, so a suite that carefully kept `setTimeout` real gets it faked, and anything\n * awaiting a timer never resolves.\n *\n * Measured on both runners with the same source:\n *\n * useFakeTimers({ doNotFake: ['setTimeout'] }) jest: setTimeout real Vitest: setTimeout FAKED\n * useFakeTimers({ toFake: ['Date'] }) Vitest: setTimeout real\n *\n * Found on `libs/cloud/events-notifications`: 4 tests in one file died on `Test timed out in\n * 5000ms` with nothing else to show, because the code under test awaits a real timer. The repo has\n * 2 files using `doNotFake`, the other in `apps/nest/microservices/institution`.\n *\n * ## The translation, and what it inherits\n *\n * `doNotFake: [a, b]` becomes `toFake: <everything the runner fakes by default> minus [a, b]`. The\n * default set is Vitest's, measured rather than assumed, and NOT jest's, which is wider: jest also\n * fakes `nextTick`, `queueMicrotask` and the animation-frame pair. Subtracting from Vitest's own\n * default is what every other `useFakeTimers()` call in the corpus already gets, so this keeps one\n * behaviour for the whole migration instead of two.\n */\n\nimport { alreadyInstalled } from './install-once.js'\n\n/**\n * What `vi.useFakeTimers()` replaces when told nothing, measured on Vitest 4 by comparing each\n * global before and after the call.\n */\nconst FAKED_BY_DEFAULT = [\n 'setTimeout',\n 'clearTimeout',\n 'setInterval',\n 'clearInterval',\n 'setImmediate',\n 'clearImmediate',\n 'Date',\n 'performance',\n 'hrtime',\n] as const\n\n/** The options both runners take, plus the one only jest knows. */\ninterface TimerOptions {\n toFake?: string[]\n doNotFake?: string[]\n}\n\n/**\n * Turn \"leave these alone\" into \"fake those\", leaving anything else untouched.\n *\n * Exported for its own test: the translation is the whole rule, and asserting it directly says more\n * than asserting that a wrapper was installed.\n */\nexport function withoutJestOnlyOptions<T>(options: T): T {\n const given = options as TimerOptions | undefined\n if (given?.doNotFake === undefined) return options\n\n const { doNotFake, ...rest } = given\n const base = rest.toFake ?? [...FAKED_BY_DEFAULT]\n\n return { ...rest, toFake: base.filter((timer) => !doNotFake.includes(timer)) } as T\n}\n\n/**\n * The one function this touches, named by its shape rather than by Vitest's type.\n *\n * `Options` is the caller's own parameter type: the wrapper hands back exactly what it was given,\n * minus the option Vitest does not know, so it must not narrow what the runner accepts.\n */\ninterface FakeTimerApi<Options> {\n useFakeTimers: (options?: Options) => unknown\n}\n\n/** Wrap `vi.useFakeTimers` so a jest-shaped options object still means what it said. */\nexport function installJestFakeTimerOptions<Options>(vi: FakeTimerApi<Options>): void {\n // A setup file runs once per TEST FILE, and without isolation `vi` is the worker's\n // single object, so a second pass would wrap the first pass's wrapper.\n if (alreadyInstalled(vi, 'jest-fake-timer-options')) return\n const inherited = vi.useFakeTimers.bind(vi)\n vi.useFakeTimers = (options?: Options) => inherited(withoutJestOnlyOptions(options))\n}\n","/**\n * The `jest` global, kept alive for helpers that a migrating module is not allowed to edit.\n *\n * ## Why a module cannot solve this for itself\n *\n * The codemod rewrites a module's own test files. It does not rewrite files in OTHER projects, and\n * it must not: a shared helper is imported by modules still on jest, so migrating it would break\n * them, and leaving it breaks the migrated one. That is the constraint the whole per-module plan\n * rests on.\n *\n * But those helpers call the jest API at MODULE scope. `libs/front/tests/src/mocks/**` does\n * `jest.fn()` when it is imported, before any test runs, so a migrated module dies on\n * `ReferenceError: jest is not defined` the moment it imports one.\n *\n * Measured repo-wide, excluding documentation: **50 files use the jest API without being test\n * files**, in `jest.setup.js`, `*.mock.ts`, `*.test-helper.ts`, `*.test-wrapper.ts`. Three\n * independent hand migrations reached this same line without knowing about each other:\n * `libs/front/components` (8 shared helpers, 16 sites), `apps/nest/microservices/mission` and\n * `apps/nest/backends-for-frontends/admin`.\n *\n * ## It is `vi`, not a fake jest\n *\n * The global IS Vitest's `vi`, so anything Vitest does not have keeps failing loudly:\n * `jest.requireActual` and `jest.isolateModules` are still errors, and a module relying on them\n * still has to be migrated properly. Handing over a hand-written imitation would turn those into\n * silent wrong behaviour, which is the opposite of the point.\n *\n * ## ⚠️ What it does NOT cover, and this bound is measured\n *\n * `jest.mock()`. Vitest hoists mock registrations above the imports by scanning the source\n * STATICALLY, and that scan only recognises the receivers `vi` and `vitest` (`@vitest/mocker`,\n * `hoistMocksPlugin`). A `jest.mock()` left in place is therefore NOT hoisted: it runs after the\n * imports it was meant to intercept and does nothing at all, in silence. Measured on\n * `apps/front/front-legacy`, where 216 of 427 files call it.\n *\n * So this covers a helper that CALLS the jest API. It does not make an unmigrated test file work,\n * and the codemod's rename stays load-bearing rather than cosmetic.\n */\n\n/** The part of `vi` this installs. Vitest's own type is not needed to say it. */\ntype JestLike = object\n\n/**\n * Put `vi` on `globalThis` under the name `jest`.\n *\n * Assigned rather than defined with a getter: a helper may well write to it (`jest.fn = ...` in a\n * test double), and a getter-only property would throw where jest allowed it.\n */\nexport function installJestGlobal(vi: JestLike): void {\n ;(globalThis as Record<string, unknown>).jest = vi\n}\n","/**\n * What `mockReset()` leaves behind, which is where the two runners disagree most dangerously.\n *\n * jest REMOVES the implementation: a reset spy returns `undefined` and the real function is not\n * called. Vitest puts the ORIGINAL implementation back: a reset spy calls the real function again.\n *\n * Measured on both runners with the same source:\n *\n * after resetAllMocks() on a spy jest: undefined Vitest: the real function\n * after mockReset() on a spy jest: undefined Vitest: the real function\n * after mockReset() on fn(impl) jest: undefined Vitest: impl\n *\n * The shape this breaks is ordinary and common: a suite spies on a provider in `beforeAll` and\n * resets its mocks in `beforeEach`. Under jest the provider stayed neutralised for every test.\n * Under Vitest the first `beforeEach` hands the real provider back, and every test after it runs\n * the real code. Measured on `libs/cloud/events-notifications`, that meant real HTTP: 19 tests\n * failed on `captured a request without a matching request handler` for the hermes API and 23 more\n * timed out waiting on it. Nothing in any report named a reset.\n *\n * 240 files in this repo both spy and reset, in every family: 111 under `apps/nest`, 77 under\n * `libs/cloud`, 31 under `apps/front`.\n *\n * ## Why here and not in the 240 files\n *\n * Because a codemod would have to decide, per spy, whether the suite wanted the real function\n * back, and the answer is in the test's intent rather than in its text. The runner-level rule is\n * the one that was true for all 240 while they were written, so restoring it is the transcription\n * and rewriting them would be the guess.\n *\n * Vitest routes `vi.resetAllMocks()` through each mock's own `mockReset`, measured, so overriding\n * that method covers the bulk form as well as the direct one. `mockRestore` is untouched: it puts\n * the original back under both runners, which is what it is for.\n */\n\nimport { alreadyInstalled } from './install-once.js'\n\n/** The part of a mock this file touches. Vitest's own types are not needed to say it. */\ninterface ResettableMock {\n mockReset: () => unknown\n mockImplementation: (fn: (...args: unknown[]) => unknown) => unknown\n}\n\nfunction isResettable(value: unknown): value is ResettableMock {\n return (\n typeof value === 'function' &&\n typeof (value as Partial<ResettableMock>).mockReset === 'function' &&\n typeof (value as Partial<ResettableMock>).mockImplementation === 'function'\n )\n}\n\n/**\n * Make one mock forget its implementation on reset, as jest's did.\n *\n * The override is installed on the instance rather than on a prototype: mocks are functions with\n * their own properties, and there is no shared prototype to reach.\n */\nfunction resetLikeJest<T>(mock: T): T {\n if (!isResettable(mock)) return mock\n\n const inherited = mock.mockReset.bind(mock)\n mock.mockReset = () => {\n inherited()\n mock.mockImplementation(() => undefined)\n return mock\n }\n return mock\n}\n\n/** The two factories a suite gets its mocks from. */\ntype MockFactories = { fn: (...args: never[]) => unknown; spyOn: (...args: never[]) => unknown }\n\n/**\n * Wrap `vi.fn` and `vi.spyOn` so everything they hand out resets the way jest's did.\n *\n * Called with the `vi` a setup file imports, so nothing here reaches for a global.\n */\nexport function installJestMockReset(vi: MockFactories): void {\n // A setup file runs once per TEST FILE, and without isolation `vi` is the worker's\n // single object, so a second pass would wrap the first pass's wrapper.\n if (alreadyInstalled(vi, 'jest-mock-reset')) return\n for (const name of ['fn', 'spyOn'] as const) {\n const factory = vi[name].bind(vi) as (...args: never[]) => unknown\n vi[name] = ((...args: never[]) => resetLikeJest(factory(...args))) as MockFactories[typeof name]\n }\n}\n","/**\n * A rejected value that is a FUNCTION, which jest calls and Vitest does not.\n *\n * ## The divergence, measured rather than reasoned\n *\n * jest's `toThrow` decides what was thrown like this: under `.rejects` it uses the rejection\n * reason ONLY when that reason is an Error. Otherwise it falls through to the ordinary branch,\n * sees a function, and CALLS it, asserting on whatever that call throws.\n *\n * Probed on this repo under jest 29, with controls:\n *\n * reject(() => { throw new Error('some error') })\n * await expect(…).rejects.toThrow(new Error('some error')) -> passes\n * await expect(…).rejects.toThrow(new Error('other text')) -> fails\n * await expect(…).rejects.toThrow('other text') -> fails\n *\n * The two controls are what make the first line mean something: jest is not passing everything,\n * it really is comparing the message of the error the CALL produced.\n *\n * Vitest treats the rejection reason as the thrown value, so the assertion is made against a\n * function. A function has no `message`, and the failure reads\n * `Cannot read properties of undefined (reading 'indexOf')`, which names nothing near the cause.\n *\n * ## Where it shows, and what it is worth\n *\n * Measured across the nest campaign: 4 tests in 3 modules.\n * `apps/nest/microservices/worker` (1), `apps/nest/microservices/institution` (2) and\n * `apps/nest/microservices/mission` (1). All four write the same shape, a mock rejecting with a\n * thunk that throws, or a `throw <a function>`.\n *\n * ## What this changes, and what it cannot\n *\n * Only a case that CANNOT work today: under `.rejects`, a reason that is a function and not an\n * Error. Vitest has no useful behaviour there, so nothing that passes today changes shape. An\n * Error reason, a string, an object, a rejected value of any other kind, and every assertion\n * outside `.rejects` all reach Vitest's own matcher untouched.\n *\n * ⚠️ It is jest's behaviour, not a good one. A suite reaching it is asserting on a function it\n * never meant to hand over, and it passed by accident of the runner. Reproducing it is what keeps\n * the migration honest: the gate promises the suite means the same thing afterwards, and a test\n * that was green cannot be turned red by us and called a finding. The teams own the cleanup.\n */\nimport { chai } from 'vitest'\n\n/** The part of a chai assertion this touches. Chai's own types are not needed to say it. */\ninterface AssertionLike {\n _obj: unknown\n}\n\ntype Matcher = (this: AssertionLike, ...args: unknown[]) => unknown\n\n/**\n * What calling the function throws, or the function itself when it throws nothing.\n *\n * Returning it unchanged matters: a function that completes is not \"nothing was thrown\", and\n * handing Vitest the same value it had leaves the report exactly as it would have been.\n */\nfunction thrownByCalling(candidate: () => unknown): unknown {\n try {\n candidate()\n } catch (thrown) {\n return thrown\n }\n return candidate\n}\n\nexport function installJestRejectedFunction(): void {\n const { Assertion, util } = chai as unknown as {\n Assertion: { prototype: Record<string, unknown> }\n util: { flag(object: unknown, key: string): unknown }\n }\n\n for (const name of ['toThrow', 'toThrowError']) {\n const original = Assertion.prototype[name] as Matcher | undefined\n if (typeof original !== 'function') continue\n\n Assertion.prototype[name] = function patched(this: AssertionLike, ...args: unknown[]): unknown {\n const reason = this._obj\n const rejected = util.flag(this, 'promise') === 'rejects'\n\n if (rejected && typeof reason === 'function' && !(reason instanceof Error)) {\n this._obj = thrownByCalling(reason as () => unknown)\n }\n\n return original.apply(this, args)\n } as unknown as Matcher\n }\n}\n","/**\n * `JEST_WORKER_ID`, which jest set and Vitest does not, and which suites use to stop sharing.\n *\n * ## What breaks without it, and how quietly\n *\n * A suite that needs a resource of its own per worker derives its name from this variable. In this\n * repository:\n *\n * const workerId = process.env.JEST_WORKER_ID ?? '1'\n *\n * Under jest that is 1, 2, 3, so each worker gets `recruitment_test_1`, `_2`, `_3` and nobody\n * treads on anybody. Under Vitest the variable does not exist, every worker takes the `'1'`\n * fallback, and they all write to ONE database while truncating it under each other.\n *\n * Measured on `apps/nest/microservices/recruitment` in the whole-monorepo sweep: 143 of 1228\n * functional tests failed, and not on assertions. `Foreign key constraint violated`,\n * `Unique constraint failed`, and PostgreSQL `40P01 deadlock detected` with two processes waiting\n * on each other's locks. Every log line said `Using database: recruitment_test_1`, whatever the\n * worker. The same suite is green on its own branch, because there it is the only thing running.\n *\n * Nothing in the migration mentions this variable, which is what makes it the migration's problem:\n * a suite that did not share a database now shares one, and says so only through the failures of\n * whichever test lost the race.\n *\n * ## Why `VITEST_POOL_ID` and not `VITEST_WORKER_ID`\n *\n * Measured, because the two look interchangeable and are not. With 8 files and `maxWorkers: 2`:\n *\n * VITEST_POOL_ID 1, 2 bounded by the worker count, 1-based\n * VITEST_WORKER_ID 0, 1, 2, 3, 4, 5... one per file, 0-based\n *\n * `JEST_WORKER_ID` is bounded and 1-based, so `VITEST_POOL_ID` is its counterpart. Taking the\n * other one would turn \"one database per worker\" into one database per FILE, and a 0 would collide\n * with nothing but read as falsy in code that tests the variable rather than compares it.\n */\n\n/** The part of the environment this touches. Node's own types are not needed to say it. */\ninterface Environment {\n JEST_WORKER_ID?: string\n VITEST_POOL_ID?: string\n}\n\n/**\n * Point `JEST_WORKER_ID` at Vitest's pool id, unless something already set it.\n *\n * Set from a SETUP file, which runs before the test file's module graph is evaluated, because a\n * module reading the variable at import time would otherwise capture the fallback.\n */\nexport function installWorkerId(env: Environment): void {\n if (env.JEST_WORKER_ID !== undefined) return\n if (env.VITEST_POOL_ID === undefined) return\n\n env.JEST_WORKER_ID = env.VITEST_POOL_ID\n}\n"],"mappings":"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","names":["vi","vi","expect","vi","vi","vi"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/roles/test/setup/msw-lifecycle.ts"],"sourcesContent":["/**\n * The msw lifecycle, as its OWN setup file, loaded only by a module that uses msw.\n *\n * ## Why it moved out of the shared setup\n *\n * It used to run for every adopted module, and that was wrong in a way only a real module could\n * show. Installing msw means patching `http` and `https` and refusing any request that matches no\n * handler. A module that never asked for msw gets that refusal anyway, and its own traffic is what\n * pays.\n *\n * Measured on `libs/nest/starter`, whose suite talks to a Fastify server it starts itself:\n *\n * its own config 11 of 11\n * the same config plus the shared setup 10 of 11, `TypeError: Invalid URL`\n * inside @mswjs/interceptors' fetch interceptor\n *\n * That module has no msw handler anywhere. The failing test does\n * `new URL(`${prefix}/events`, await app.getUrl())` and fetches its own server, and the\n * interceptor cannot read that URL.\n *\n * So the rule is the one asked for at the start: intelligent per module, decided from what the\n * module's files actually contain, not applied to everybody because it is convenient.\n *\n * ## What it does NOT change\n *\n * The lifecycle itself is untouched: `listen` at evaluation time AND in `beforeAll`,\n * `onUnhandledRequest: 'error'` carried across, no `resetHandlers`. `./msw.js` carries the\n * measurement for each of those.\n */\nimport { afterAll, afterEach, beforeAll } from 'vitest'\n\nimport { installMswLifecycle, server } from './msw.js'\n\ninstallMswLifecycle({ beforeAll, afterEach, afterAll })\n\n/**\n * Re-exported so a suite can add its own handlers, which is how all 600 files that touch msw here\n * already work: `server.use(...)` inside a test.\n */\nexport { server }\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../../src/roles/test/setup/msw-lifecycle.ts"],"sourcesContent":["/**\n * The msw lifecycle, as its OWN setup file, loaded only by a module that uses msw.\n *\n * ## Why it moved out of the shared setup\n *\n * It used to run for every adopted module, and that was wrong in a way only a real module could\n * show. Installing msw means patching `http` and `https` and refusing any request that matches no\n * handler. A module that never asked for msw gets that refusal anyway, and its own traffic is what\n * pays.\n *\n * Measured on `libs/nest/starter`, whose suite talks to a Fastify server it starts itself:\n *\n * its own config 11 of 11\n * the same config plus the shared setup 10 of 11, `TypeError: Invalid URL`\n * inside @mswjs/interceptors' fetch interceptor\n *\n * That module has no msw handler anywhere. The failing test does\n * `new URL(`${prefix}/events`, await app.getUrl())` and fetches its own server, and the\n * interceptor cannot read that URL.\n *\n * So the rule is the one asked for at the start: intelligent per module, decided from what the\n * module's files actually contain, not applied to everybody because it is convenient.\n *\n * ## What it does NOT change\n *\n * The lifecycle itself is untouched: `listen` at evaluation time AND in `beforeAll`,\n * `onUnhandledRequest: 'error'` carried across, no `resetHandlers`. `./msw.js` carries the\n * measurement for each of those.\n */\nimport { afterAll, afterEach, beforeAll } from 'vitest'\n\nimport { installMswLifecycle, server } from './msw.js'\n\ninstallMswLifecycle({ beforeAll, afterEach, afterAll })\n\n/**\n * Re-exported so a suite can add its own handlers, which is how all 600 files that touch msw here\n * already work: `server.use(...)` inside a test.\n */\nexport { server }\n"],"mappings":";;;;;;;AA6BA,SAAS,UAAU,WAAW,iBAAiB;AAI/C,oBAAoB,EAAE,WAAW,WAAW,SAAS,CAAC;","names":[]}
|