@cosmicdrift/kumiko-testing 0.316.0 → 0.317.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 +8 -4
- package/package.json +20 -6
- package/src/bunfig.ts +70 -4
- package/src/changes.json +13 -0
- package/src/preload/dom.ts +143 -0
package/README.md
CHANGED
|
@@ -11,12 +11,16 @@ const persisted = await seedTenant(stack, { persist: true }); // real rows via t
|
|
|
11
11
|
await tenant.api.writeOk("my-feature:write:thing:create", { title: "x" });
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
- `preload/{temporal,ci-log,scrub-env,env,real}`: bunfig `preload` entries. `scrub-env` removes
|
|
14
|
+
- `preload/{temporal,ci-log,scrub-env,env,real,dom}`: bunfig `preload` entries. `scrub-env` removes
|
|
15
15
|
`PROVIDER_ENV_KEYS` (unit default); `env` adds the localhost service defaults (integration);
|
|
16
|
-
`real` refuses to run without `KUMIKO_REAL_PROVIDERS=1` and in CI, and keeps the keys
|
|
16
|
+
`real` refuses to run without `KUMIKO_REAL_PROVIDERS=1` and in CI, and keeps the keys; `dom`
|
|
17
|
+
registers happy-dom (window/document/HTMLElement) while preserving Bun's native
|
|
18
|
+
fetch/Request/Response, and cleans up (`@testing-library/react` unmount, Radix DOM/style leaks,
|
|
19
|
+
`window.location`) after every test.
|
|
17
20
|
- `kumiko-testing bunfig [--dom] [--coverage]`: writes `bunfig.toml`,
|
|
18
|
-
`bunfig.integration.toml` and `bunfig.real.toml` (plus `bunfig.dom.toml`). `--dom`
|
|
19
|
-
|
|
21
|
+
`bunfig.integration.toml` and `bunfig.real.toml` (plus `bunfig.dom.toml`). `--dom` adds
|
|
22
|
+
`preload/dom` (happy-dom + testing-library/react cleanup); `@happy-dom/global-registrator` and
|
|
23
|
+
`@testing-library/react` are optional peer dependencies, install them when you use `--dom`.
|
|
20
24
|
- `kumiko-testing integration [--parallel N] [--timings <file>]`: runs `*.integration.test.ts`
|
|
21
25
|
with the 15s budget. `--parallel` only when you ask for it, and then with `--no-isolate`:
|
|
22
26
|
bun 1.4.0's implicit `--isolate` leaks native memory per file until CI workers are OOM-killed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.317.0",
|
|
4
4
|
"description": "Test template for Kumiko apps: per-flow seedTenant, app test stack, canonical test preloads, bunfig generator and integration runner.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -46,6 +46,10 @@
|
|
|
46
46
|
"types": "./src/preload/real.ts",
|
|
47
47
|
"default": "./src/preload/real.ts"
|
|
48
48
|
},
|
|
49
|
+
"./preload/dom": {
|
|
50
|
+
"types": "./src/preload/dom.ts",
|
|
51
|
+
"default": "./src/preload/dom.ts"
|
|
52
|
+
},
|
|
49
53
|
"./scaffold": {
|
|
50
54
|
"types": "./src/scaffold.ts",
|
|
51
55
|
"default": "./src/scaffold.ts"
|
|
@@ -63,21 +67,31 @@
|
|
|
63
67
|
"kumiko-testing": "./bin/kumiko-testing.ts"
|
|
64
68
|
},
|
|
65
69
|
"dependencies": {
|
|
66
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
67
|
-
"@cosmicdrift/kumiko-dev-server": "0.
|
|
68
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
70
|
+
"@cosmicdrift/kumiko-bundled-features": "0.317.0",
|
|
71
|
+
"@cosmicdrift/kumiko-dev-server": "0.317.0",
|
|
72
|
+
"@cosmicdrift/kumiko-framework": "0.317.0",
|
|
69
73
|
"zod": "^4.4.3"
|
|
70
74
|
},
|
|
71
75
|
"peerDependencies": {
|
|
72
|
-
"@
|
|
76
|
+
"@happy-dom/global-registrator": "^20.11.2",
|
|
77
|
+
"@playwright/test": "^1.62.1",
|
|
78
|
+
"@testing-library/react": "^16.3.2"
|
|
73
79
|
},
|
|
74
80
|
"peerDependenciesMeta": {
|
|
81
|
+
"@happy-dom/global-registrator": {
|
|
82
|
+
"optional": true
|
|
83
|
+
},
|
|
75
84
|
"@playwright/test": {
|
|
76
85
|
"optional": true
|
|
86
|
+
},
|
|
87
|
+
"@testing-library/react": {
|
|
88
|
+
"optional": true
|
|
77
89
|
}
|
|
78
90
|
},
|
|
79
91
|
"devDependencies": {
|
|
80
|
-
"@
|
|
92
|
+
"@happy-dom/global-registrator": "^20.11.2",
|
|
93
|
+
"@playwright/test": "^1.62.1",
|
|
94
|
+
"@testing-library/react": "^16.3.2"
|
|
81
95
|
},
|
|
82
96
|
"publishConfig": {
|
|
83
97
|
"registry": "https://registry.npmjs.org",
|
package/src/bunfig.ts
CHANGED
|
@@ -25,7 +25,7 @@ export type BunfigOptions = {
|
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
const PRELOAD_PREFIX = "@cosmicdrift/kumiko-testing/preload";
|
|
28
|
-
const DOM_PRELOAD =
|
|
28
|
+
const DOM_PRELOAD = `${PRELOAD_PREFIX}/dom`;
|
|
29
29
|
|
|
30
30
|
const PRELOADS = {
|
|
31
31
|
unit: [`${PRELOAD_PREFIX}/temporal`, `${PRELOAD_PREFIX}/scrub-env`],
|
|
@@ -125,6 +125,57 @@ export type BunfigMergeResult =
|
|
|
125
125
|
| { readonly ok: true; readonly content: string }
|
|
126
126
|
| { readonly ok: false; readonly unknownKeys: readonly UnknownBunfigKey[] };
|
|
127
127
|
|
|
128
|
+
// Unlike an unknown key, which blocks the merge, an array entry the template
|
|
129
|
+
// stops emitting stays on disk silently until the generator excludes it by
|
|
130
|
+
// name, as it does below for the superseded DOM preload path.
|
|
131
|
+
const MERGED_ARRAY_KEYS = ["preload", "pathIgnorePatterns", "coveragePathIgnorePatterns"] as const;
|
|
132
|
+
type MergedArrayKey = (typeof MERGED_ARRAY_KEYS)[number];
|
|
133
|
+
|
|
134
|
+
const CURRENT_PACKAGE_PRELOADS = new Set<string>([...Object.values(PRELOADS).flat(), DOM_PRELOAD]);
|
|
135
|
+
|
|
136
|
+
// Old app-local path of DOM_PRELOAD; excluded so an app that hasn't
|
|
137
|
+
// regenerated doesn't run DOM setup twice.
|
|
138
|
+
const SUPERSEDED_PRELOADS = new Set<string>(["./test-setup/dom.preload.ts"]);
|
|
139
|
+
|
|
140
|
+
function isKeepableExtra(key: MergedArrayKey, entry: string): boolean {
|
|
141
|
+
if (key !== "preload") return true;
|
|
142
|
+
if (SUPERSEDED_PRELOADS.has(entry)) return false;
|
|
143
|
+
if (entry.startsWith(PRELOAD_PREFIX)) return CURRENT_PACKAGE_PRELOADS.has(entry);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function parseTestSection(content: string): Readonly<Record<string, unknown>> {
|
|
148
|
+
const parsed = Bun.TOML.parse(content) as { test?: Record<string, unknown> };
|
|
149
|
+
return parsed.test ?? {};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function arrayField(
|
|
153
|
+
testSection: Readonly<Record<string, unknown>>,
|
|
154
|
+
key: MergedArrayKey,
|
|
155
|
+
): readonly string[] {
|
|
156
|
+
const value = testSection[key];
|
|
157
|
+
if (typeof value === "string") return [value];
|
|
158
|
+
return Array.isArray(value)
|
|
159
|
+
? value.filter((item): item is string => typeof item === "string")
|
|
160
|
+
: [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// TOML comments attached to an extra on disk don't survive: only the parsed
|
|
164
|
+
// string value is carried over, not the source lines around it.
|
|
165
|
+
function appendArrayExtras(
|
|
166
|
+
generated: string,
|
|
167
|
+
key: MergedArrayKey,
|
|
168
|
+
extras: readonly string[],
|
|
169
|
+
): string {
|
|
170
|
+
if (extras.length === 0) return generated;
|
|
171
|
+
const extraLines = extras.map((item) => ` ${JSON.stringify(item)},`).join("\n");
|
|
172
|
+
const pattern = new RegExp(`^(${key} = \\[\\n)([\\s\\S]*?)(\\n\\])`, "m");
|
|
173
|
+
return generated.replace(
|
|
174
|
+
pattern,
|
|
175
|
+
(_match, open: string, body: string, close: string) => `${open}${body}\n${extraLines}${close}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
128
179
|
/**
|
|
129
180
|
* Merges freshly generated bunfig content with whatever an app already has
|
|
130
181
|
* on disk. A section this run's `generated` output doesn't touch at all
|
|
@@ -134,7 +185,11 @@ export type BunfigMergeResult =
|
|
|
134
185
|
* template doesn't own this run is never silently dropped. A key inside a
|
|
135
186
|
* section the template DOES emit that isn't in this run's own version of
|
|
136
187
|
* that section (e.g. `concurrency`, `timeout`, kumiko-framework#3120) fails
|
|
137
|
-
* the merge instead of silently disappearing.
|
|
188
|
+
* the merge instead of silently disappearing. Within an array key the
|
|
189
|
+
* template does emit on both sides (`preload`, `pathIgnorePatterns`,
|
|
190
|
+
* `coveragePathIgnorePatterns`), entries the app appended on top of the
|
|
191
|
+
* generated ones survive the regeneration instead of being overwritten with
|
|
192
|
+
* the generated array alone.
|
|
138
193
|
*/
|
|
139
194
|
export function mergeBunfig(generated: string, existingContent: string): BunfigMergeResult {
|
|
140
195
|
const generatedKeysBySection = new Map(
|
|
@@ -154,11 +209,22 @@ export function mergeBunfig(generated: string, existingContent: string): BunfigM
|
|
|
154
209
|
}
|
|
155
210
|
}
|
|
156
211
|
if (unknownKeys.length > 0) return { ok: false, unknownKeys };
|
|
157
|
-
|
|
212
|
+
const generatedTest = parseTestSection(generated);
|
|
213
|
+
const existingTest = parseTestSection(existingContent);
|
|
214
|
+
let content = generated;
|
|
215
|
+
for (const key of MERGED_ARRAY_KEYS) {
|
|
216
|
+
const generatedValues = arrayField(generatedTest, key);
|
|
217
|
+
const existingValues = arrayField(existingTest, key);
|
|
218
|
+
const extras = existingValues.filter(
|
|
219
|
+
(item) => !generatedValues.includes(item) && isKeepableExtra(key, item),
|
|
220
|
+
);
|
|
221
|
+
content = appendArrayExtras(content, key, extras);
|
|
222
|
+
}
|
|
223
|
+
if (foreignBlocks.length === 0) return { ok: true, content };
|
|
158
224
|
const foreignText = foreignBlocks
|
|
159
225
|
.map((block) => `[${block.header}]\n${block.lines.join("\n").replace(/\n+$/, "")}\n`)
|
|
160
226
|
.join("\n");
|
|
161
|
-
return { ok: true, content: `${
|
|
227
|
+
return { ok: true, content: `${content}\n${foreignText}` };
|
|
162
228
|
}
|
|
163
229
|
|
|
164
230
|
export function renderBunfigFiles(opts: BunfigOptions = {}): Readonly<Record<string, string>> {
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.317.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "mergeBunfig keeps app-local array extras instead of overwriting them",
|
|
6
|
+
"detail": "mergeBunfig only checked whether a key such as `preload` existed on both\nsides of the merge, not whether its array value matched. Regenerating a\nbunfig therefore silently replaced an app's `[test].preload`,\n`pathIgnorePatterns` or `coveragePathIgnorePatterns` array with the\ngenerated one, dropping any entry the app had appended by hand (measured\nagainst publicstatus's real bunfig.toml, which carries exactly this case\nfor env.preload.ts/codegen.preload.ts and says so in a comment).\nmergeBunfig now diffs the parsed array for each of these three keys and\nappends entries only present on disk after the generated ones, so\n`kumiko-testing bunfig` is safe to rerun on an app that extended one of\nthem. A `./test-setup/dom.preload.ts` entry left over from before the\nDOM preload became a package export (see the DOM-preload-package\nchangeset in this same release) is not carried forward as an extra,\nsince keeping it would preload the same DOM setup twice.\nA package preload an app deliberately adds from another variant (say\n`preload/env` in its unit bunfig) is kept; an unrecognized or superseded\npackage preload path is dropped instead of kept as an extra, so a rename\ninside the package doesn't leave both the old and new path preloaded. A\npreload written as a single string rather than an array is read the same\nas a one-item array.\nTwo known limits: a comment attached to an extra in the source bunfig is\nnot preserved, only the value; and the template can no longer remove a\n`pathIgnorePatterns`/`coveragePathIgnorePatterns` entry from a consumer,\nsince either key has no equivalent to preload's exclusion list, so a\npattern it stops emitting stays in every consumer's bunfig until removed\nby hand. Unlike an unknown key, which blocks the merge, a stale array\nentry is kept silently rather than failing loudly."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.317.0",
|
|
10
|
+
"type": "breaking",
|
|
11
|
+
"title": "bunfig --dom preloads the package's own DOM setup instead of an app-local file",
|
|
12
|
+
"detail": "The package now exports `./preload/dom`, the happy-dom GlobalRegistrator\nsetup (IS_REACT_ACT_ENVIRONMENT, Bun's native fetch/Request/Response\npreserved over happy-dom's broken cookie header, testing-library/react\ncleanup, Radix pointer-capture/scrollIntoView stubs, the inspect-size\nlimit for waitFor) that each app using --dom previously copied into its\nown ./test-setup/dom.preload.ts. `kumiko-testing bunfig --dom` now generates\nbunfig.dom.toml with @cosmicdrift/kumiko-testing/preload/dom instead of\nthe app-local path. @happy-dom/global-registrator and\n@testing-library/react are optional peer dependencies, same as\n@playwright/test for ./e2e. This reverses kumiko-framework#3128, which\nkept the file as an app-local copy because at the time it was\nworkspace-internal, not a published contract. In practice the copies\nnever diverged on purpose, only by drift: publicstatus, phronexsis and\nkumiko-enterprise carried older, partial copies of the framework's own\ntest-setup/dom.preload.ts (missing the inspect-size fix for #3082), and\nonly offlot-app matched the framework's code (comments reworded, no\nfunctional diff). The framework's own copy is now deleted; the\nframework runs the same package preload it ships.",
|
|
13
|
+
"migration": "In every bunfig file that lists \"./test-setup/dom.preload.ts\"\n(publicstatus, phronexsis, offlot-app and kumiko-enterprise each have\nexactly one, their bunfig.dom.toml), replace that line with\n\"@cosmicdrift/kumiko-testing/preload/dom\", then delete the now-\nunreferenced ./test-setup/dom.preload.ts. Ensure\n@happy-dom/global-registrator and @testing-library/react are in the\napp's devDependencies (all four already have both). Running\n`kumiko-testing bunfig --dom` instead also works: mergeBunfig now keeps\nan app-local extra preload (publicstatus's\nenv.preload.ts/codegen.preload.ts) across the regen instead of dropping\nit, and treats an existing \"./test-setup/dom.preload.ts\" entry as\nsuperseded rather than an extra, so it doesn't end up preloaded twice\nalongside the new package path. The generated preload array's own\nentries stay first; a carried-over extra lands after them, which can\nmove it relative to entries that used to sit between the template's\npreloads and the app's own (publicstatus: dom.preload.ts used to run\nlast, after env/codegen; the package preload now runs before them)."
|
|
14
|
+
},
|
|
2
15
|
{
|
|
3
16
|
"version": "0.316.0",
|
|
4
17
|
"type": "fix",
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// DOM polyfill for tests that use testing-library/react or Radix components.
|
|
2
|
+
// happy-dom's global-registrator attaches window/document/HTMLElement to
|
|
3
|
+
// globalThis. Plain Node tests are unaffected, no code path changes for them.
|
|
4
|
+
|
|
5
|
+
import { afterEach } from "bun:test";
|
|
6
|
+
import { GlobalRegistrator } from "@happy-dom/global-registrator";
|
|
7
|
+
|
|
8
|
+
declare global {
|
|
9
|
+
// React's own typings don't declare this property on globalThis.
|
|
10
|
+
var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// react-dom checks IS_REACT_ACT_ENVIRONMENT to suppress act() warnings.
|
|
14
|
+
// vitest set this automatically via @testing-library/react; with bun:test we
|
|
15
|
+
// set it explicitly.
|
|
16
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
17
|
+
|
|
18
|
+
// Register idempotently, so a second preload entry doesn't crash.
|
|
19
|
+
if (typeof globalThis.window === "undefined") {
|
|
20
|
+
// Preserve Bun's native fetch/Request/Response/Headers before happy-dom
|
|
21
|
+
// overwrites them. happy-dom ships its own Request implementation whose
|
|
22
|
+
// headers.get("cookie") returns null, which breaks Hono's getCookie() in
|
|
23
|
+
// every auth/csrf/sse test. We only want the DOM globals (window,
|
|
24
|
+
// document, HTMLElement) from happy-dom, not the fetch API.
|
|
25
|
+
const bunRequest = globalThis.Request;
|
|
26
|
+
const bunResponse = globalThis.Response;
|
|
27
|
+
const bunHeaders = globalThis.Headers;
|
|
28
|
+
const bunFetch = globalThis.fetch;
|
|
29
|
+
const bunAbortController = globalThis.AbortController;
|
|
30
|
+
const bunAbortSignal = globalThis.AbortSignal;
|
|
31
|
+
const bunWritableStream = globalThis.WritableStream;
|
|
32
|
+
const bunTransformStream = globalThis.TransformStream;
|
|
33
|
+
// The url option sets window.location to http://localhost/. Without it
|
|
34
|
+
// happy-dom defaults to about:blank, so history.pushState/replaceState
|
|
35
|
+
// doesn't work (invalid origin) and window.location.pathname stays
|
|
36
|
+
// "blank", breaking every router/nav test.
|
|
37
|
+
GlobalRegistrator.register({ url: "http://localhost/" });
|
|
38
|
+
globalThis.Request = bunRequest;
|
|
39
|
+
globalThis.Response = bunResponse;
|
|
40
|
+
globalThis.Headers = bunHeaders;
|
|
41
|
+
globalThis.fetch = bunFetch;
|
|
42
|
+
globalThis.AbortController = bunAbortController;
|
|
43
|
+
globalThis.AbortSignal = bunAbortSignal;
|
|
44
|
+
globalThis.WritableStream = bunWritableStream;
|
|
45
|
+
globalThis.TransformStream = bunTransformStream;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// @testing-library/dom/dist/screen.js checks document.body at module import
|
|
49
|
+
// time. A static import would evaluate screen before happy-dom registers, so
|
|
50
|
+
// every screen query would throw TypeError. Hence require() only after the
|
|
51
|
+
// registration above.
|
|
52
|
+
const { cleanup } = require("@testing-library/react/pure") as {
|
|
53
|
+
cleanup: () => void;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const HTML_PRINT_LIMIT = 2000;
|
|
57
|
+
|
|
58
|
+
// Pointer-capture APIs are missing in happy-dom, same as in jsdom. Radix-UI
|
|
59
|
+
// (DropdownMenu/Select/Popover triggers) calls them, so nothing opens in
|
|
60
|
+
// tests without the polyfill.
|
|
61
|
+
if (typeof globalThis.HTMLElement !== "undefined") {
|
|
62
|
+
const proto = globalThis.HTMLElement.prototype as unknown as Record<string | symbol, unknown>;
|
|
63
|
+
if (proto["hasPointerCapture"] === undefined) proto["hasPointerCapture"] = () => false;
|
|
64
|
+
if (proto["setPointerCapture"] === undefined) proto["setPointerCapture"] = () => undefined;
|
|
65
|
+
if (proto["releasePointerCapture"] === undefined)
|
|
66
|
+
proto["releasePointerCapture"] = () => undefined;
|
|
67
|
+
if (proto["scrollIntoView"] === undefined) proto["scrollIntoView"] = () => undefined;
|
|
68
|
+
|
|
69
|
+
// Without this, printing a happy-dom node walks its whole object graph:
|
|
70
|
+
// ownerDocument plus every React fiber property, which is 15 MB of string
|
|
71
|
+
// for a two-element tree and 0.5-1.7 s per call. A failed assertion inside
|
|
72
|
+
// waitFor pays that on every poll and blocks the loop long enough to starve
|
|
73
|
+
// React's commit and waitFor's own timeout (#3082).
|
|
74
|
+
const inspect = Symbol.for("nodejs.util.inspect.custom");
|
|
75
|
+
if (proto[inspect] === undefined) {
|
|
76
|
+
proto[inspect] = function (this: HTMLElement): string {
|
|
77
|
+
const html = this.outerHTML;
|
|
78
|
+
return html.length > HTML_PRINT_LIMIT ? `${html.slice(0, HTML_PRINT_LIMIT)}…` : html;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Auto-cleanup after every test (DOM pollution guard): bun test runs all
|
|
84
|
+
// test files in one process, so without afterEach, React components from
|
|
85
|
+
// file N stay mounted in file N+1's DOM.
|
|
86
|
+
//
|
|
87
|
+
// FIVE leak sources:
|
|
88
|
+
//
|
|
89
|
+
// a) testing-library/react container: cleanup() unmounts and removes
|
|
90
|
+
// every container node render() created.
|
|
91
|
+
//
|
|
92
|
+
// b) body.replaceChildren(): clears containers not created via
|
|
93
|
+
// testing-library/react (e.g. #root via ReactDOM.createRoot +
|
|
94
|
+
// renderShell). Must run after cleanup(), since React needs its nodes
|
|
95
|
+
// to unmount.
|
|
96
|
+
//
|
|
97
|
+
// c) Radix DismissableLayer sets body.style.pointerEvents='none' when
|
|
98
|
+
// opening a Dialog/Popover/Dropdown.
|
|
99
|
+
//
|
|
100
|
+
// d) Radix-injected style tags in head.
|
|
101
|
+
//
|
|
102
|
+
// e) window.location / history.pushState.
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
// skip: this preload module runs its own afterEach unconditionally; plain
|
|
105
|
+
// unit test files (no --dom) never get happy-dom registered, so document
|
|
106
|
+
// stays undefined and there is nothing to clean up.
|
|
107
|
+
if (typeof globalThis.document === "undefined") return;
|
|
108
|
+
|
|
109
|
+
// (a) React cleanup first: unmounts every testing-library-rendered
|
|
110
|
+
// component via ReactDOM.unmountComponentAtNode. Must happen before
|
|
111
|
+
// any DOM manipulation because React needs its nodes.
|
|
112
|
+
cleanup();
|
|
113
|
+
|
|
114
|
+
const doc = globalThis.document;
|
|
115
|
+
// skip: happy-dom always creates body, but a defensive guard keeps this
|
|
116
|
+
// afterEach from throwing if a future registration option removes it.
|
|
117
|
+
if (!doc.body) return;
|
|
118
|
+
|
|
119
|
+
// (b) Remaining nodes are not actively removed: cleanup() from
|
|
120
|
+
// testing-library/react clears all render() containers. Non-standard
|
|
121
|
+
// containers (#root via createRoot) must be cleaned by the tests
|
|
122
|
+
// themselves or via afterEach in the test file. body.replaceChildren()
|
|
123
|
+
// triggers async React effects without an act context.
|
|
124
|
+
|
|
125
|
+
// (c) Radix leak: reset the body inline style
|
|
126
|
+
doc.body.style.pointerEvents = "";
|
|
127
|
+
|
|
128
|
+
// (d) Radix-injected style tags
|
|
129
|
+
for (const el of doc.head.querySelectorAll("style")) {
|
|
130
|
+
const style = el as HTMLStyleElement;
|
|
131
|
+
if (style.id?.startsWith("radix-")) {
|
|
132
|
+
style.remove();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// (e) Reset window.location to happy-dom's initial url. replaceState
|
|
137
|
+
// instead of pushState, otherwise a history stack of 3000+ entries
|
|
138
|
+
// accumulates across all tests (memory leak, plus it breaks tests that
|
|
139
|
+
// check history.length).
|
|
140
|
+
if (typeof globalThis.history !== "undefined") {
|
|
141
|
+
globalThis.history.replaceState(null, "", "http://localhost/");
|
|
142
|
+
}
|
|
143
|
+
});
|