@ctrl/dom-jest-environment 0.1.2 → 0.1.3

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.
Files changed (2) hide show
  1. package/dist/index.js +1 -276
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,276 +1 @@
1
- import { constants, createContext } from "node:vm";
2
- import { LegacyFakeTimers, ModernFakeTimers } from "@jest/fake-timers";
3
- import { ModuleMocker } from "jest-mock";
4
- import { installCommonGlobals } from "jest-util";
5
- import * as dom from "@ctrl/dom";
6
- import * as internal from "@ctrl/dom/internal";
7
-
8
- // src/failure-snapshot.ts
9
- /** The values of CTRL_DOM_DEBUG that turn agent mode on (agent-debug.ts, envDebugFormat). */
10
- function debugEnabled() {
11
- const value = process.env.CTRL_DOM_DEBUG?.trim().toLowerCase();
12
- return value === "aria" || value === "compact" || value === "html";
13
- }
14
- /** A `handleTestEvent` for the environment, or undefined when CTRL_DOM_DEBUG is off. */
15
- function failureSnapshotHandler(getDocument) {
16
- if (!debugEnabled()) return void 0;
17
- let agent = null;
18
- return async (event) => {
19
- if (event.name !== "test_start" && event.name !== "test_fn_failure") return;
20
- agent ??= await import("@ctrl/dom/testing-library/agent");
21
- const document = getDocument();
22
- if (document == null) return;
23
- if (event.name === "test_start") {
24
- agent.resetFailureSnapshot(document);
25
- return;
26
- }
27
- const error = event.error;
28
- const message = error !== null && typeof error === "object" && typeof error.message === "string" ? error.message : "";
29
- const snapshot = agent.captureFailureSnapshot(document);
30
- if (snapshot !== null) agent.appendToErrorMessage(error, agent.formatFailureSnapshot(snapshot, message));
31
- };
32
- }
33
-
34
- // src/index.ts
35
- /** The vm-global key the bridges and shims read. */
36
- const CTRL_DOM_MODULES = Symbol.for("ctrl-dom.dom.modules");
37
- const JSDOM_ABSENT_GLOBALS = /* @__PURE__ */ new Set([
38
- "fetch",
39
- "Request",
40
- "Response"
41
- ]);
42
- const NODE_GLOBAL_DENY = /* @__PURE__ */ new Set([
43
- "GLOBAL",
44
- "root",
45
- "global",
46
- "globalThis",
47
- "Buffer",
48
- "ArrayBuffer",
49
- "Uint8Array",
50
- "jest-symbol-do-not-touch",
51
- "localStorage",
52
- "sessionStorage"
53
- ]);
54
- const LAZY_NODE_GLOBALS = /* @__PURE__ */ new Map([
55
- ["fetch", true],
56
- ["Request", false],
57
- ["Response", false],
58
- ["Headers", false],
59
- ["FormData", false],
60
- ["WebSocket", false],
61
- ["EventSource", false],
62
- ["MessageEvent", false],
63
- ["CloseEvent", false]
64
- ]);
65
- let nodeGlobals = null;
66
- /** [name, descriptor] for Node's globals (descriptor null: lazy, see LAZY_NODE_GLOBALS). */
67
- function nodeGlobalDescriptors() {
68
- return nodeGlobals ??= Object.getOwnPropertyNames(globalThis).filter((name) => !NODE_GLOBAL_DENY.has(name)).map((name) => [name, LAZY_NODE_GLOBALS.has(name) ? null : Object.getOwnPropertyDescriptor(globalThis, name)]);
69
- }
70
- /**
71
- * Copy Node's globals onto the test global the way jest-environment-node does: configurable ones
72
- * as getters that read the outer value on first use and replace themselves with a data property;
73
- * names the new realm already defines (Object, Array, ...) keep the realm's own.
74
- */
75
- function installNodeGlobals(global, skip) {
76
- const realmOwn = new Set(Object.getOwnPropertyNames(global));
77
- const host = globalThis;
78
- for (const [name, descriptor] of nodeGlobalDescriptors()) {
79
- if (realmOwn.has(name) || skip.has(name)) continue;
80
- const enumerable = descriptor === null ? LAZY_NODE_GLOBALS.get(name) : descriptor.enumerable;
81
- if (descriptor === null || descriptor.configurable) {
82
- const settle = (value) => Object.defineProperty(global, name, {
83
- value,
84
- writable: true,
85
- enumerable,
86
- configurable: true
87
- });
88
- Object.defineProperty(global, name, {
89
- get() {
90
- const value = host[name];
91
- settle(value);
92
- return value;
93
- },
94
- set: settle,
95
- enumerable,
96
- configurable: true
97
- });
98
- } else Object.defineProperty(global, name, descriptor);
99
- }
100
- global.global = global;
101
- global.Buffer = Buffer;
102
- global.ArrayBuffer = ArrayBuffer;
103
- global.Uint8Array = Uint8Array;
104
- }
105
- /** Same default URL as jest-environment-jsdom. */
106
- const DEFAULT_URL = "http://localhost/";
107
- /**
108
- * Exceptions nobody handled (listener exceptions, reportError(), React's uncaught errors) are
109
- * re-emitted on the worker process, which jest-circus turns into a test failure — what
110
- * jest-environment-jsdom does with its window "error" listener.
111
- * https://html.spec.whatwg.org/#report-an-exception
112
- */
113
- function reportToHost(error) {
114
- process.emit("uncaughtException", error, "uncaughtException");
115
- }
116
- /**
117
- * jsdom's "Not implemented" diagnostics (navigation, window.alert, form submission, ...; only
118
- * under the jsdom profiles, @ctrl/dom window/not-implemented.ts), forwarded to the test file's
119
- * console as jest-environment-jsdom does: with jsdom 26 (`sendTo` omitting jsdom errors, then
120
- * its "jsdomError" listener) once, as the Error; with jsdom 27+ (`forwardTo`, then the same
121
- * listener) as `error.stack`, then as the Error.
122
- * https://github.com/jestjs/jest/blob/v30.4.1/packages/jest-environment-jsdom-abstract/src/index.ts
123
- */
124
- function forwardNotImplemented(global) {
125
- return (error, compat) => {
126
- const console = global.console;
127
- if (compat !== "jsdom-26") console.error(error.stack, error.detail);
128
- console.error(error);
129
- };
130
- }
131
- const ERROR_CONSTRUCTORS = [
132
- "Error",
133
- "TypeError",
134
- "RangeError",
135
- "SyntaxError",
136
- "ReferenceError",
137
- "EvalError",
138
- "URIError"
139
- ];
140
- /**
141
- * Our DOM runs in the worker's realm while test code runs in the vm context, so a TypeError
142
- * thrown by @ctrl/dom is not `instanceof` the test realm's TypeError (`expect(fn).toThrow(TypeError)`
143
- * would fail). Give the context's error constructors a Symbol.hasInstance that also accepts the
144
- * outer realm's instances; errors created in the context keep working as before.
145
- */
146
- function bridgeErrorRealms(global) {
147
- for (const name of ERROR_CONSTRUCTORS) {
148
- const inner = global[name];
149
- const outer = globalThis[name];
150
- if (typeof inner !== "function" || inner === outer) continue;
151
- Object.defineProperty(inner, Symbol.hasInstance, {
152
- value: (value) => Function.prototype[Symbol.hasInstance].call(inner, value) || value instanceof outer,
153
- configurable: true
154
- });
155
- }
156
- }
157
- function createDom(window) {
158
- return {
159
- window,
160
- reconfigure(settings) {
161
- if (settings.windowTop !== void 0) throw new TypeError("@ctrl/dom-jest-environment: dom.reconfigure() does not support windowTop");
162
- if (settings.url === void 0) return;
163
- const url = URL.parse(String(settings.url));
164
- if (url === null) throw new TypeError(`Could not parse "${settings.url}" as a URL`);
165
- window._setURL(url);
166
- }
167
- };
168
- }
169
- var CtrlDomEnvironment = class {
170
- context;
171
- global;
172
- fakeTimers;
173
- fakeTimersModern;
174
- moduleMocker;
175
- customExportConditions;
176
- ctrlDomWindow;
177
- /** jest-environment-jsdom's `dom`, reduced to what suites use (see CtrlDomDom). */
178
- dom;
179
- /** The file's timers (see CtrlDomTestEnvironmentOptions.timeSkip). */
180
- clock;
181
- /** Stops adding the page and the clock to @ctrl/sentry-tests' failure reports. */
182
- #unsubscribeFailures = null;
183
- constructor(config, _environmentContext) {
184
- const { projectConfig } = config;
185
- const { customExportConditions, nodeFetch, timeSkip, ...windowOptions } = projectConfig.testEnvironmentOptions ?? {};
186
- this.customExportConditions = customExportConditions ?? ["browser"];
187
- const context = createContext(constants.DONT_CONTEXTIFY);
188
- this.context = context;
189
- const global = context;
190
- this.global = global;
191
- installNodeGlobals(global, nodeFetch === true ? /* @__PURE__ */ new Set() : JSDOM_ABSENT_GLOBALS);
192
- installCommonGlobals(this.global, projectConfig.globals);
193
- const realmSymbol = global.Symbol;
194
- if (!("asyncDispose" in realmSymbol)) {
195
- Object.defineProperty(realmSymbol, "asyncDispose", { value: Symbol.for("nodejs.asyncDispose") });
196
- Object.defineProperty(realmSymbol, "dispose", { value: Symbol.for("nodejs.dispose") });
197
- }
198
- global.Error.stackTraceLimit = 100;
199
- const window = dom.createWindow({
200
- ...windowOptions,
201
- compat: windowOptions.compat ?? "jsdom-26",
202
- url: windowOptions.url ?? DEFAULT_URL
203
- });
204
- this.ctrlDomWindow = window;
205
- this.dom = createDom(window);
206
- internal.captureInterfaceBaseline();
207
- dom.installGlobals(window, global);
208
- Object.defineProperty(global, CTRL_DOM_MODULES, {
209
- value: {
210
- dom,
211
- internal
212
- },
213
- configurable: true,
214
- enumerable: false,
215
- writable: false
216
- });
217
- bridgeErrorRealms(global);
218
- const envSkip = process.env.CTRL_DOM_TIME_SKIP;
219
- const skip = envSkip === "1" ? true : envSkip === "0" ? false : timeSkip !== false;
220
- this.clock = internal.installClock(new internal.VirtualClock({ skip }), global).clock;
221
- this.moduleMocker = new ModuleMocker(this.global);
222
- this.fakeTimers = new LegacyFakeTimers({
223
- config: projectConfig,
224
- global: this.global,
225
- moduleMocker: this.moduleMocker,
226
- timerConfig: {
227
- idToRef: (id) => id,
228
- refToId: (ref) => ref
229
- }
230
- });
231
- this.fakeTimersModern = new ModernFakeTimers({
232
- config: projectConfig,
233
- global: this.global
234
- });
235
- internal.guardInterfaceDefinitions(global);
236
- internal.setUncaughtExceptionHandler(reportToHost);
237
- internal.setNotImplementedHandler(forwardNotImplemented(global));
238
- const onTestEvent = failureSnapshotHandler(() => this.ctrlDomWindow?.document);
239
- if (onTestEvent !== void 0) this.handleTestEvent = onTestEvent;
240
- this.#unsubscribeFailures = internal.subscribeTestFailures(() => this.ctrlDomWindow?.document);
241
- }
242
- async setup() {}
243
- exportConditions() {
244
- return this.customExportConditions;
245
- }
246
- getVmContext() {
247
- return this.context;
248
- }
249
- /**
250
- * Closes the window (cancelling its timers, frames, idle callbacks and tasks) and undoes the file's
251
- * patches to the shared interface prototypes. The globals stay installed, like under
252
- * jest-environment-jsdom where the global *is* the window: work a test file left behind (React's
253
- * scheduler runs on Node's setImmediate) can still read `window`/`document` instead of
254
- * crashing the worker with a ReferenceError. The vm context is discarded with the file.
255
- */
256
- async teardown() {
257
- this.#unsubscribeFailures?.();
258
- this.#unsubscribeFailures = null;
259
- internal.setUncaughtExceptionHandler(null);
260
- internal.setNotImplementedHandler(null);
261
- this.fakeTimers?.dispose();
262
- this.fakeTimersModern?.dispose();
263
- this.ctrlDomWindow?.close();
264
- this.ctrlDomWindow = null;
265
- this.clock?.dispose();
266
- this.clock = null;
267
- this.dom = null;
268
- internal.restoreInterfaces();
269
- this.context = null;
270
- this.fakeTimers = null;
271
- this.fakeTimersModern = null;
272
- }
273
- };
274
- const TestEnvironment = CtrlDomEnvironment;
275
-
276
- export { CTRL_DOM_MODULES, TestEnvironment, CtrlDomEnvironment as default };
1
+ import{constants as e,createContext as t}from"node:vm";import{LegacyFakeTimers as n,ModernFakeTimers as r}from"@jest/fake-timers";import{ModuleMocker as i}from"jest-mock";import{installCommonGlobals as a}from"jest-util";import*as o from"@ctrl/dom";import*as s from"@ctrl/dom/internal";function debugEnabled(){let e=process.env.CTRL_DOM_DEBUG?.trim().toLowerCase();return e===`aria`||e===`compact`||e===`html`}function failureSnapshotHandler(e){if(!debugEnabled())return;let t=null;return async n=>{if(n.name!==`test_start`&&n.name!==`test_fn_failure`)return;t??=await import(`@ctrl/dom/testing-library/agent`);let r=e();if(r==null)return;if(n.name===`test_start`){t.resetFailureSnapshot(r);return}let i=n.error,a=typeof i==`object`&&i&&typeof i.message==`string`?i.message:``,o=t.captureFailureSnapshot(r);o!==null&&t.appendToErrorMessage(i,t.formatFailureSnapshot(o,a))}}const c=Symbol.for(`ctrl-dom.dom.modules`),l=/* @__PURE__ */ new Set([`fetch`,`Request`,`Response`]),u=/* @__PURE__ */ new Set([`GLOBAL`,`root`,`global`,`globalThis`,`Buffer`,`ArrayBuffer`,`Uint8Array`,`jest-symbol-do-not-touch`,`localStorage`,`sessionStorage`]),d=/* @__PURE__ */ new Map([[`fetch`,!0],[`Request`,!1],[`Response`,!1],[`Headers`,!1],[`FormData`,!1],[`WebSocket`,!1],[`EventSource`,!1],[`MessageEvent`,!1],[`CloseEvent`,!1]]);let f=null;function nodeGlobalDescriptors(){return f??=Object.getOwnPropertyNames(globalThis).filter(e=>!u.has(e)).map(e=>[e,d.has(e)?null:Object.getOwnPropertyDescriptor(globalThis,e)])}function installNodeGlobals(e,t){let n=new Set(Object.getOwnPropertyNames(e)),r=globalThis;for(let[i,a]of nodeGlobalDescriptors()){if(n.has(i)||t.has(i))continue;let o=a===null?d.get(i):a.enumerable;if(a===null||a.configurable){let settle=t=>Object.defineProperty(e,i,{value:t,writable:!0,enumerable:o,configurable:!0});Object.defineProperty(e,i,{get(){let e=r[i];return settle(e),e},set:settle,enumerable:o,configurable:!0})}else Object.defineProperty(e,i,a)}e.global=e,e.Buffer=Buffer,e.ArrayBuffer=ArrayBuffer,e.Uint8Array=Uint8Array}function reportToHost(e){process.emit(`uncaughtException`,e,`uncaughtException`)}function forwardNotImplemented(e){return(t,n)=>{let r=e.console;n!==`jsdom-26`&&r.error(t.stack,t.detail),r.error(t)}}const p=[`Error`,`TypeError`,`RangeError`,`SyntaxError`,`ReferenceError`,`EvalError`,`URIError`];function bridgeErrorRealms(e){for(let t of p){let n=e[t],r=globalThis[t];typeof n==`function`&&n!==r&&Object.defineProperty(n,Symbol.hasInstance,{value:e=>Function.prototype[Symbol.hasInstance].call(n,e)||e instanceof r,configurable:!0})}}function createDom(e){return{window:e,reconfigure(t){if(t.windowTop!==void 0)throw TypeError(`@ctrl/dom-jest-environment: dom.reconfigure() does not support windowTop`);if(t.url===void 0)return;let n=URL.parse(String(t.url));if(n===null)throw TypeError(`Could not parse "${t.url}" as a URL`);e._setURL(n)}}}var CtrlDomEnvironment=class{context;global;fakeTimers;fakeTimersModern;moduleMocker;customExportConditions;ctrlDomWindow;dom;clock;#e=null;constructor(u,d){let{projectConfig:f}=u,{customExportConditions:p,nodeFetch:m,timeSkip:h,...g}=f.testEnvironmentOptions??{};this.customExportConditions=p??[`browser`];let _=t(e.DONT_CONTEXTIFY);this.context=_;let v=_;this.global=v,installNodeGlobals(v,m===!0?/* @__PURE__ */ new Set:l),a(this.global,f.globals);let y=v.Symbol;`asyncDispose`in y||(Object.defineProperty(y,"asyncDispose",{value:Symbol.for(`nodejs.asyncDispose`)}),Object.defineProperty(y,"dispose",{value:Symbol.for(`nodejs.dispose`)})),v.Error.stackTraceLimit=100;let b=o.createWindow({...g,compat:g.compat??`jsdom-26`,url:g.url??`http://localhost/`});this.ctrlDomWindow=b,this.dom=createDom(b),s.captureInterfaceBaseline(),o.installGlobals(b,v),Object.defineProperty(v,c,{value:{dom:o,internal:s},configurable:!0,enumerable:!1,writable:!1}),bridgeErrorRealms(v);let x=process.env.CTRL_DOM_TIME_SKIP,S=x===`1`||x!==`0`&&h!==!1;this.clock=s.installClock(new s.VirtualClock({skip:S}),v).clock,this.moduleMocker=new i(this.global),this.fakeTimers=new n({config:f,global:this.global,moduleMocker:this.moduleMocker,timerConfig:{idToRef:e=>e,refToId:e=>e}}),this.fakeTimersModern=new r({config:f,global:this.global}),s.guardInterfaceDefinitions(v),s.setUncaughtExceptionHandler(reportToHost),s.setNotImplementedHandler(forwardNotImplemented(v));let C=failureSnapshotHandler(()=>this.ctrlDomWindow?.document);C!==void 0&&(this.handleTestEvent=C),this.#e=s.subscribeTestFailures(()=>this.ctrlDomWindow?.document)}async setup(){}exportConditions(){return this.customExportConditions}getVmContext(){return this.context}async teardown(){this.#e?.(),this.#e=null,s.setUncaughtExceptionHandler(null),s.setNotImplementedHandler(null),this.fakeTimers?.dispose(),this.fakeTimersModern?.dispose(),this.ctrlDomWindow?.close(),this.ctrlDomWindow=null,this.clock?.dispose(),this.clock=null,this.dom=null,s.restoreInterfaces(),this.context=null,this.fakeTimers=null,this.fakeTimersModern=null}};const m=CtrlDomEnvironment;export{c as CTRL_DOM_MODULES,m as TestEnvironment,CtrlDomEnvironment as default};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctrl/dom-jest-environment",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Jest environment for @ctrl/dom, the fast DOM for testing React components (testEnvironment: \"@ctrl/dom-jest-environment\", or preset: \"@ctrl/dom\").",
5
5
  "keywords": [
6
6
  "jest",
@@ -52,7 +52,7 @@
52
52
  "jest-util": "30.4.1"
53
53
  },
54
54
  "peerDependencies": {
55
- "@ctrl/dom": "^0.1.4",
55
+ "@ctrl/dom": "^0.1.5",
56
56
  "jest": ">=30.0.0"
57
57
  }
58
58
  }