jest-environment-webgl-node 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.
package/README.md CHANGED
@@ -1,12 +1,31 @@
1
+ <p align="center">
2
+ <a href="https://npmjs.com/package/jest-environment-webgl-node"><img src="https://img.shields.io/npm/v/jest-environment-webgl-node.svg" alt="npm package"></a>
3
+ <a href="https://www.npmtrends.com/jest-environment-webgl-node"><img src="https://img.shields.io/npm/dw/jest-environment-webgl-node.svg" alt="npm downloads"></a>
4
+ <a href="https://codecov.io/gh/bhouston/jest-gpu"><img src="https://codecov.io/gh/bhouston/jest-gpu/branch/main/graph/badge.svg" alt="coverage"></a>
5
+ <a href="https://discord.gg/fwupDN493R"><img src="https://img.shields.io/badge/chat-discord-blue?style=flat&logo=discord" alt="discord chat"></a>
6
+ <a href="https://jest-gpu.ben3d.ca/"><img src="https://img.shields.io/badge/docs-read-blue?style=flat" alt="documentation"></a>
7
+ </p>
8
+ <br/>
9
+
1
10
  # jest-environment-webgl-node
2
11
 
3
- Real, headless WebGL 1 and WebGL 2 inside Jest, with no browser. Powered by
4
- [`@onirenaud/node-webgl`](https://github.com/RenaudRohlinger/node-webgl) (Chrome's ANGLE, statically linked).
12
+ > Real WebGL 1 and 2 tests in plain Jest, with no browser.
13
+
14
+ - 🖥️ Real WebGL 1 and 2
15
+ - 🚫 No Browser or Mocks
16
+ - 🧩 Fresh DOM Per Test File
17
+ - ⏱️ Works With Jest Fake Timers
18
+ - 📦 Dual ESM and CommonJS
19
+ - ⚡ Up to 2.4x Faster Than Browsers
20
+
21
+ Real, headless WebGL 1 and WebGL 2 inside Jest. `document.createElement('canvas').getContext('webgl2')` returns a
22
+ GPU-backed context, so three.js, regl, pixi and plain WebGL code run unchanged. Native GPU testing runs up to 2.4x
23
+ faster than the same tests in a browser, see [the write-up](https://ben3d.ca/blog/native-gpu-testing-for-vitest-and-jest).
24
+ Powered by [`@onirenaud/node-webgl`](https://github.com/RenaudRohlinger/node-webgl) (Chrome's ANGLE, statically linked).
5
25
 
6
- The environment installs `window`, `document`, `HTMLCanvasElement`, `Image`, `requestAnimationFrame`,
7
- the `WebGL*` classes and a file-reading `fetch()` on the test's sandbox global, and removes them
8
- after the test file finishes. `document.createElement('canvas').getContext('webgl2')` returns a
9
- GPU-backed context, so three.js, regl, pixi and plain WebGL code run unchanged.
26
+ Part of [jest-gpu](https://github.com/bhouston/jest-gpu), which also provides the WebGPU environment.
27
+
28
+ [Read the Docs to Learn More](https://jest-gpu.ben3d.ca/docs/webgl/) for the full WebGL guide, CommonJS and ESM setup, CI configuration and the API reference.
10
29
 
11
30
  ## Install
12
31
 
@@ -48,6 +67,11 @@ animation-frame queue. Animation frames use Jest's sandbox timers, so `jest.useF
48
67
  `jest.advanceTimersByTime()` control them; pending frames are cancelled during environment
49
68
  teardown.
50
69
 
70
+ Teardown also disposes canvases created by `document.createElement`, `document.createElementNS`,
71
+ `new HTMLCanvasElement` and `new OffscreenCanvas`, releasing their native contexts and GPU resources.
72
+ Canvases are retained until teardown; call `canvas.dispose()` when finished using one. Canvases or
73
+ contexts created directly through imports from `@onirenaud/node-webgl` remain the caller's responsibility.
74
+
51
75
  File fetching is enabled by default. Relative paths resolve from that test file's `baseDir`
52
76
  (default: `process.cwd()`), while HTTP, data and blob URLs use Node's native `fetch`. Set
53
77
  `fetch: false` to use native fetch for every URL. The environment never replaces host globals.
@@ -69,14 +93,18 @@ On Linux CI install Mesa (`apt-get install libegl1 libgles2 libgl1-mesa-dri`) an
69
93
 
70
94
  ## Using Vitest?
71
95
 
72
- The equivalent package for Vitest is
96
+ The equivalent package for Vitest (see the [vitest-gpu docs](https://vitest-gpu.ben3d.ca/)) is
73
97
  [`vitest-environment-webgl-node`](https://github.com/bhouston/vitest-gpu/tree/main/packages/vitest-environment-webgl-node)
74
98
  from [vitest-gpu](https://github.com/bhouston/vitest-gpu).
75
99
 
76
- ## Author
100
+ ## Contribution
77
101
 
78
- Created by [Ben Houston](https://ben3d.ca) and sponsored by [Land of Assets](https://landofassets.com).
102
+ See [Contributing Guide](https://github.com/bhouston/jest-gpu/blob/main/CONTRIBUTING.md).
79
103
 
80
104
  ## License
81
105
 
82
106
  MIT
107
+
108
+ ## Author
109
+
110
+ [Ben Houston](https://ben3d.ca), Sponsored by [Land of Assets](https://landofassets.com).
package/dist/index.cjs CHANGED
@@ -164,9 +164,20 @@ function localFetch(baseDir, nativeFetch) {
164
164
  }
165
165
  };
166
166
  }
167
+ /**
168
+ * Jest environment that puts real headless WebGL 1 and 2 (ANGLE via `@onirenaud/node-webgl`) and a browser-like
169
+ * DOM on the sandbox global. Each test file gets a fresh document and animation-frame queue. Set options under
170
+ * `testEnvironmentOptions`; see {@link WebglNodeOptions}.
171
+ */
167
172
  var WebglEnvironment = class extends jest_environment_node.TestEnvironment {
168
173
  added = [];
169
174
  animationFrames = /* @__PURE__ */ new Map();
175
+ canvases = /* @__PURE__ */ new Set();
176
+ /**
177
+ * Creates the environment and installs the DOM; Jest calls this once per test file.
178
+ * @param config - Jest's environment config; the WebGL options are read from `testEnvironmentOptions`.
179
+ * @param context - Jest's environment context.
180
+ */
170
181
  constructor(config, context) {
171
182
  const options = config.projectConfig.testEnvironmentOptions;
172
183
  super(environmentConfig(config), context);
@@ -192,6 +203,19 @@ var WebglEnvironment = class extends jest_environment_node.TestEnvironment {
192
203
  g.window = g;
193
204
  g.self = g;
194
205
  g.document.defaultView = g;
206
+ const track = (value) => {
207
+ if (value instanceof _onirenaud_node_webgl.Canvas) this.canvases.add(value);
208
+ return value;
209
+ };
210
+ for (const name of ["HTMLCanvasElement", "OffscreenCanvas"]) {
211
+ const Canvas = g[name];
212
+ g[name] = new Proxy(Canvas, { construct: (target, args, newTarget) => track(Reflect.construct(target, args, newTarget)) });
213
+ }
214
+ const document = g.document;
215
+ for (const name of ["createElement", "createElementNS"]) {
216
+ const create = document[name];
217
+ document[name] = (...args) => track(create.apply(document, args));
218
+ }
195
219
  const events = new EventTarget();
196
220
  g.addEventListener = events.addEventListener.bind(events);
197
221
  g.removeEventListener = events.removeEventListener.bind(events);
@@ -214,10 +238,13 @@ var WebglEnvironment = class extends jest_environment_node.TestEnvironment {
214
238
  const nativeFetch = globalThis.fetch.bind(globalThis);
215
239
  g.fetch = domOptions.fetch === false ? nativeFetch : localFetch(domOptions.baseDir ?? process.cwd(), nativeFetch);
216
240
  }
241
+ /** Cancels pending animation frames, disposes canvases the environment created and removes the installed globals. */
217
242
  async teardown() {
218
243
  const g = this.global;
219
244
  for (const [id, clear] of this.animationFrames) clear(id);
220
245
  this.animationFrames.clear();
246
+ for (const canvas of this.canvases) canvas.dispose();
247
+ this.canvases.clear();
221
248
  for (const key of this.added) delete g[key];
222
249
  await super.teardown();
223
250
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["nodeWebGL","fileURLToPath","resolvePath","readFile","extname","TestEnvironment"],"sources":["../src/index.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { extname, resolve as resolvePath } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport * as nodeWebGL from '@onirenaud/node-webgl';\nimport type { InitOptions, InstallDOMOptions } from '@onirenaud/node-webgl';\nimport { TestEnvironment } from 'jest-environment-node';\n\n/** Set under `testEnvironmentOptions` in your Jest config. */\nexport type WebglNodeOptions = InitOptions & InstallDOMOptions;\n\ntype JestEnvironmentConfig = ConstructorParameters<typeof TestEnvironment>[0];\ntype EnvironmentContext = ConstructorParameters<typeof TestEnvironment>[1];\ntype Sandbox = Record<string, unknown>;\n\nconst WEBGL_OPTION_NAMES = new Set([\n 'api',\n 'backend',\n 'baseDir',\n 'devicePixelRatio',\n 'fetch',\n 'frameInterval',\n 'innerHeight',\n 'innerWidth',\n]);\n\nconst MIME: Record<string, string> = {\n '.basis': 'application/octet-stream',\n '.bin': 'application/octet-stream',\n '.drc': 'application/octet-stream',\n '.exr': 'image/x-exr',\n '.fbx': 'application/octet-stream',\n '.gif': 'image/gif',\n '.glb': 'model/gltf-binary',\n '.gltf': 'model/gltf+json',\n '.hdr': 'image/vnd.radiance',\n '.jpeg': 'image/jpeg',\n '.jpg': 'image/jpeg',\n '.js': 'text/javascript',\n '.json': 'application/json',\n '.ktx': 'image/ktx',\n '.ktx2': 'image/ktx2',\n '.mjs': 'text/javascript',\n '.mtl': 'text/plain',\n '.obj': 'text/plain',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n '.txt': 'text/plain',\n '.wasm': 'application/wasm',\n '.webp': 'image/webp',\n};\n\nconst DOM_NAMES = [\n 'window',\n 'self',\n 'HTMLCanvasElement',\n 'OffscreenCanvas',\n 'HTMLImageElement',\n 'Image',\n 'ImageData',\n 'ImageBitmap',\n 'createImageBitmap',\n 'FileReader',\n 'HTMLElement',\n 'Element',\n 'Node',\n 'devicePixelRatio',\n 'innerWidth',\n 'innerHeight',\n 'screen',\n 'requestAnimationFrame',\n 'cancelAnimationFrame',\n 'getComputedStyle',\n 'matchMedia',\n 'scrollTo',\n 'alert',\n 'location',\n 'document',\n ...Object.keys(nodeWebGL).filter((name) => name.startsWith('WebGL')),\n] as const;\n\nlet initializedWith: InitOptions | undefined;\n\n/**\n * The native EGL display is process-wide and node-webgl's `init()` is a no-op once it exists (the first\n * context creates it lazily), so compare explicit options against the live display rather than only\n * against what this module itself initialized.\n */\nfunction initialize({ api, backend }: InitOptions): void {\n if (!api && !backend) return;\n if (initializedWith && initializedWith.api === api && initializedWith.backend === backend) return;\n const display = nodeWebGL.getDisplayInfo();\n const mismatch =\n display &&\n ((backend && backend !== 'default' && backend !== display.backend) ||\n (api && api !== 'auto' && api !== display.api));\n if (mismatch) {\n throw new Error(\n `node-webgl is already initialized with ${JSON.stringify({ api: display.api, backend: display.backend })}; ` +\n `cannot switch this Jest worker to ${JSON.stringify({ api, backend })}`,\n );\n }\n nodeWebGL.init({ api, backend });\n initializedWith = { api, backend };\n}\n\n/** Capture a fresh node-webgl DOM while restoring every host descriptor after installation. */\nfunction createDOM(options: InstallDOMOptions): Map<string, PropertyDescriptor> {\n const before = Object.getOwnPropertyDescriptors(globalThis);\n for (const name of DOM_NAMES) Reflect.deleteProperty(globalThis, name);\n\n try {\n nodeWebGL.installDOM({ ...options, fetch: false });\n const after = Object.getOwnPropertyDescriptors(globalThis);\n return new Map(\n DOM_NAMES.flatMap((name): [string, PropertyDescriptor][] => (after[name] ? [[name, after[name]]] : [])),\n );\n } finally {\n for (const name of Object.getOwnPropertyNames(globalThis)) {\n if (!(name in before)) Reflect.deleteProperty(globalThis, name);\n }\n Object.defineProperties(globalThis, before);\n }\n}\n\nfunction environmentConfig(config: JestEnvironmentConfig): JestEnvironmentConfig {\n return {\n ...config,\n projectConfig: {\n ...config.projectConfig,\n testEnvironmentOptions: Object.fromEntries(\n Object.entries(config.projectConfig.testEnvironmentOptions).filter(([name]) => !WEBGL_OPTION_NAMES.has(name)),\n ),\n },\n };\n}\n\nfunction localFetch(baseDir: string, nativeFetch: typeof fetch): typeof fetch {\n return async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {\n const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;\n if (/^(https?|data|blob):/i.test(url)) return nativeFetch(input, init);\n const path = url.startsWith('file:') ? fileURLToPath(url) : resolvePath(baseDir, url.replace(/[?#].*$/, ''));\n try {\n const bytes = await readFile(path);\n return new Response(bytes, {\n status: 200,\n headers: { 'content-type': MIME[extname(path).toLowerCase()] ?? 'application/octet-stream' },\n });\n } catch {\n return new Response(null, { status: 404, statusText: 'Not Found' });\n }\n };\n}\n\nexport default class WebglEnvironment extends TestEnvironment {\n private readonly added: string[] = [];\n private readonly animationFrames = new Map<ReturnType<typeof setTimeout>, typeof clearTimeout>();\n\n constructor(config: JestEnvironmentConfig, context: EnvironmentContext) {\n const options = config.projectConfig.testEnvironmentOptions as WebglNodeOptions;\n super(environmentConfig(config), context);\n\n const { api, backend, ...domOptions } = options;\n initialize({ api, backend });\n\n const g = this.global as unknown as Sandbox;\n const nativeAdded = !Object.getOwnPropertyNames(g).includes('_native');\n if (nativeAdded) {\n Object.defineProperty(g, '_native', {\n value: undefined,\n writable: true,\n configurable: true,\n enumerable: false,\n });\n this.added.push('_native');\n }\n\n for (const [name, descriptor] of createDOM(domOptions)) {\n if (!(name in g)) this.added.push(name);\n Object.defineProperty(g, name, descriptor);\n }\n\n g.window = g;\n g.self = g;\n (g.document as Record<string, unknown>).defaultView = g;\n\n const events = new EventTarget();\n g.addEventListener = events.addEventListener.bind(events);\n g.removeEventListener = events.removeEventListener.bind(events);\n g.dispatchEvent = events.dispatchEvent.bind(events);\n this.added.push('addEventListener', 'removeEventListener', 'dispatchEvent');\n\n const interval = domOptions.frameInterval ?? 16;\n g.requestAnimationFrame = (callback: (time: number) => void) => {\n const id = (g.setTimeout as typeof setTimeout)(() => {\n this.animationFrames.delete(id);\n callback((g.performance as { now(): number }).now());\n }, interval);\n this.animationFrames.set(id, g.clearTimeout as typeof clearTimeout);\n return id;\n };\n g.cancelAnimationFrame = (id: ReturnType<typeof setTimeout>) => {\n const clear = this.animationFrames.get(id);\n this.animationFrames.delete(id);\n (clear ?? (g.clearTimeout as typeof clearTimeout))(id);\n };\n\n const nativeFetch = globalThis.fetch.bind(globalThis);\n g.fetch = domOptions.fetch === false ? nativeFetch : localFetch(domOptions.baseDir ?? process.cwd(), nativeFetch);\n }\n\n override async teardown(): Promise<void> {\n const g = this.global as unknown as Sandbox;\n for (const [id, clear] of this.animationFrames) clear(id);\n this.animationFrames.clear();\n for (const key of this.added) delete g[key];\n await super.teardown();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["nodeWebGL","fileURLToPath","resolvePath","readFile","extname","TestEnvironment"],"sources":["../src/index.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { extname, resolve as resolvePath } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport * as nodeWebGL from '@onirenaud/node-webgl';\nimport type { InitOptions, InstallDOMOptions } from '@onirenaud/node-webgl';\nimport { TestEnvironment } from 'jest-environment-node';\n\n/** Set under `testEnvironmentOptions` in your Jest config. */\nexport type WebglNodeOptions = InitOptions & InstallDOMOptions;\n\ntype JestEnvironmentConfig = ConstructorParameters<typeof TestEnvironment>[0];\ntype EnvironmentContext = ConstructorParameters<typeof TestEnvironment>[1];\ntype Sandbox = Record<string, unknown>;\n\nconst WEBGL_OPTION_NAMES = new Set([\n 'api',\n 'backend',\n 'baseDir',\n 'devicePixelRatio',\n 'fetch',\n 'frameInterval',\n 'innerHeight',\n 'innerWidth',\n]);\n\nconst MIME: Record<string, string> = {\n '.basis': 'application/octet-stream',\n '.bin': 'application/octet-stream',\n '.drc': 'application/octet-stream',\n '.exr': 'image/x-exr',\n '.fbx': 'application/octet-stream',\n '.gif': 'image/gif',\n '.glb': 'model/gltf-binary',\n '.gltf': 'model/gltf+json',\n '.hdr': 'image/vnd.radiance',\n '.jpeg': 'image/jpeg',\n '.jpg': 'image/jpeg',\n '.js': 'text/javascript',\n '.json': 'application/json',\n '.ktx': 'image/ktx',\n '.ktx2': 'image/ktx2',\n '.mjs': 'text/javascript',\n '.mtl': 'text/plain',\n '.obj': 'text/plain',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n '.txt': 'text/plain',\n '.wasm': 'application/wasm',\n '.webp': 'image/webp',\n};\n\nconst DOM_NAMES = [\n 'window',\n 'self',\n 'HTMLCanvasElement',\n 'OffscreenCanvas',\n 'HTMLImageElement',\n 'Image',\n 'ImageData',\n 'ImageBitmap',\n 'createImageBitmap',\n 'FileReader',\n 'HTMLElement',\n 'Element',\n 'Node',\n 'devicePixelRatio',\n 'innerWidth',\n 'innerHeight',\n 'screen',\n 'requestAnimationFrame',\n 'cancelAnimationFrame',\n 'getComputedStyle',\n 'matchMedia',\n 'scrollTo',\n 'alert',\n 'location',\n 'document',\n ...Object.keys(nodeWebGL).filter((name) => name.startsWith('WebGL')),\n] as const;\n\nlet initializedWith: InitOptions | undefined;\n\n/**\n * The native EGL display is process-wide and node-webgl's `init()` is a no-op once it exists (the first\n * context creates it lazily), so compare explicit options against the live display rather than only\n * against what this module itself initialized.\n */\nfunction initialize({ api, backend }: InitOptions): void {\n if (!api && !backend) return;\n if (initializedWith && initializedWith.api === api && initializedWith.backend === backend) return;\n const display = nodeWebGL.getDisplayInfo();\n const mismatch =\n display &&\n ((backend && backend !== 'default' && backend !== display.backend) ||\n (api && api !== 'auto' && api !== display.api));\n if (mismatch) {\n throw new Error(\n `node-webgl is already initialized with ${JSON.stringify({ api: display.api, backend: display.backend })}; ` +\n `cannot switch this Jest worker to ${JSON.stringify({ api, backend })}`,\n );\n }\n nodeWebGL.init({ api, backend });\n initializedWith = { api, backend };\n}\n\n/** Capture a fresh node-webgl DOM while restoring every host descriptor after installation. */\nfunction createDOM(options: InstallDOMOptions): Map<string, PropertyDescriptor> {\n const before = Object.getOwnPropertyDescriptors(globalThis);\n for (const name of DOM_NAMES) Reflect.deleteProperty(globalThis, name);\n\n try {\n nodeWebGL.installDOM({ ...options, fetch: false });\n const after = Object.getOwnPropertyDescriptors(globalThis);\n return new Map(\n DOM_NAMES.flatMap((name): [string, PropertyDescriptor][] => (after[name] ? [[name, after[name]]] : [])),\n );\n } finally {\n for (const name of Object.getOwnPropertyNames(globalThis)) {\n if (!(name in before)) Reflect.deleteProperty(globalThis, name);\n }\n Object.defineProperties(globalThis, before);\n }\n}\n\nfunction environmentConfig(config: JestEnvironmentConfig): JestEnvironmentConfig {\n return {\n ...config,\n projectConfig: {\n ...config.projectConfig,\n testEnvironmentOptions: Object.fromEntries(\n Object.entries(config.projectConfig.testEnvironmentOptions).filter(([name]) => !WEBGL_OPTION_NAMES.has(name)),\n ),\n },\n };\n}\n\nfunction localFetch(baseDir: string, nativeFetch: typeof fetch): typeof fetch {\n return async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {\n const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;\n if (/^(https?|data|blob):/i.test(url)) return nativeFetch(input, init);\n const path = url.startsWith('file:') ? fileURLToPath(url) : resolvePath(baseDir, url.replace(/[?#].*$/, ''));\n try {\n const bytes = await readFile(path);\n return new Response(bytes, {\n status: 200,\n headers: { 'content-type': MIME[extname(path).toLowerCase()] ?? 'application/octet-stream' },\n });\n } catch {\n return new Response(null, { status: 404, statusText: 'Not Found' });\n }\n };\n}\n\n/**\n * Jest environment that puts real headless WebGL 1 and 2 (ANGLE via `@onirenaud/node-webgl`) and a browser-like\n * DOM on the sandbox global. Each test file gets a fresh document and animation-frame queue. Set options under\n * `testEnvironmentOptions`; see {@link WebglNodeOptions}.\n */\nexport default class WebglEnvironment extends TestEnvironment {\n private readonly added: string[] = [];\n private readonly animationFrames = new Map<ReturnType<typeof setTimeout>, typeof clearTimeout>();\n private readonly canvases = new Set<nodeWebGL.Canvas>();\n\n /**\n * Creates the environment and installs the DOM; Jest calls this once per test file.\n * @param config - Jest's environment config; the WebGL options are read from `testEnvironmentOptions`.\n * @param context - Jest's environment context.\n */\n constructor(config: JestEnvironmentConfig, context: EnvironmentContext) {\n const options = config.projectConfig.testEnvironmentOptions as WebglNodeOptions;\n super(environmentConfig(config), context);\n\n const { api, backend, ...domOptions } = options;\n initialize({ api, backend });\n\n const g = this.global as unknown as Sandbox;\n const nativeAdded = !Object.getOwnPropertyNames(g).includes('_native');\n if (nativeAdded) {\n Object.defineProperty(g, '_native', {\n value: undefined,\n writable: true,\n configurable: true,\n enumerable: false,\n });\n this.added.push('_native');\n }\n\n for (const [name, descriptor] of createDOM(domOptions)) {\n if (!(name in g)) this.added.push(name);\n Object.defineProperty(g, name, descriptor);\n }\n\n g.window = g;\n g.self = g;\n (g.document as Record<string, unknown>).defaultView = g;\n\n // Native contexts are held in node-webgl's process-wide registry, without a GC finalizer.\n // Keep environment-created canvases alive until we can explicitly release their contexts.\n const track = <T>(value: T): T => {\n if (value instanceof nodeWebGL.Canvas) this.canvases.add(value);\n return value;\n };\n for (const name of ['HTMLCanvasElement', 'OffscreenCanvas'] as const) {\n const Canvas = g[name] as typeof nodeWebGL.Canvas;\n g[name] = new Proxy(Canvas, {\n construct: (target, args, newTarget) => track(Reflect.construct(target, args, newTarget)),\n });\n }\n const document = g.document as Record<'createElement' | 'createElementNS', (...args: unknown[]) => unknown>;\n for (const name of ['createElement', 'createElementNS'] as const) {\n const create = document[name];\n document[name] = (...args: unknown[]) => track(create.apply(document, args));\n }\n\n const events = new EventTarget();\n g.addEventListener = events.addEventListener.bind(events);\n g.removeEventListener = events.removeEventListener.bind(events);\n g.dispatchEvent = events.dispatchEvent.bind(events);\n this.added.push('addEventListener', 'removeEventListener', 'dispatchEvent');\n\n const interval = domOptions.frameInterval ?? 16;\n g.requestAnimationFrame = (callback: (time: number) => void) => {\n const id = (g.setTimeout as typeof setTimeout)(() => {\n this.animationFrames.delete(id);\n callback((g.performance as { now(): number }).now());\n }, interval);\n this.animationFrames.set(id, g.clearTimeout as typeof clearTimeout);\n return id;\n };\n g.cancelAnimationFrame = (id: ReturnType<typeof setTimeout>) => {\n const clear = this.animationFrames.get(id);\n this.animationFrames.delete(id);\n (clear ?? (g.clearTimeout as typeof clearTimeout))(id);\n };\n\n const nativeFetch = globalThis.fetch.bind(globalThis);\n g.fetch = domOptions.fetch === false ? nativeFetch : localFetch(domOptions.baseDir ?? process.cwd(), nativeFetch);\n }\n\n /** Cancels pending animation frames, disposes canvases the environment created and removes the installed globals. */\n override async teardown(): Promise<void> {\n const g = this.global as unknown as Sandbox;\n for (const [id, clear] of this.animationFrames) clear(id);\n this.animationFrames.clear();\n for (const canvas of this.canvases) canvas.dispose();\n this.canvases.clear();\n for (const key of this.added) delete g[key];\n await super.teardown();\n }\n}\n"],"mappings":"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"}
package/dist/index.d.cts CHANGED
@@ -5,10 +5,22 @@ import { TestEnvironment } from "jest-environment-node";
5
5
  export type WebglNodeOptions = InitOptions & InstallDOMOptions;
6
6
  type JestEnvironmentConfig = ConstructorParameters<typeof TestEnvironment>[0];
7
7
  type EnvironmentContext = ConstructorParameters<typeof TestEnvironment>[1];
8
+ /**
9
+ * Jest environment that puts real headless WebGL 1 and 2 (ANGLE via `@onirenaud/node-webgl`) and a browser-like
10
+ * DOM on the sandbox global. Each test file gets a fresh document and animation-frame queue. Set options under
11
+ * `testEnvironmentOptions`; see {@link WebglNodeOptions}.
12
+ */
8
13
  export default class WebglEnvironment extends TestEnvironment {
9
14
  private readonly added;
10
15
  private readonly animationFrames;
16
+ private readonly canvases;
17
+ /**
18
+ * Creates the environment and installs the DOM; Jest calls this once per test file.
19
+ * @param config - Jest's environment config; the WebGL options are read from `testEnvironmentOptions`.
20
+ * @param context - Jest's environment context.
21
+ */
11
22
  constructor(config: JestEnvironmentConfig, context: EnvironmentContext);
23
+ /** Cancels pending animation frames, disposes canvases the environment created and removes the installed globals. */
12
24
  teardown(): Promise<void>;
13
25
  }
14
26
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../src/index.ts"],"mappings":";;;;YAQY,mBAAmB,cAAc;KAExC,wBAAwB,6BAA6B;KACrD,qBAAqB,6BAA6B;qBA8IlC,yBAAyB;mBAC3B;mBACA;EAEjB,YAAY,QAAQ,uBAAuB,SAAS;EAqDrC,YAAY"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/index.ts"],"mappings":";;;;YAQY,mBAAmB,cAAc;KAExC,wBAAwB,6BAA6B;KACrD,qBAAqB,6BAA6B;;;;;;qBAmJlC,yBAAyB;mBAC3B;mBACA;mBACA;;;;;;EAOjB,YAAY,QAAQ,uBAAuB,SAAS;;EAwErC,YAAY"}
package/dist/index.d.mts CHANGED
@@ -5,10 +5,22 @@ import { TestEnvironment } from "jest-environment-node";
5
5
  export type WebglNodeOptions = InitOptions & InstallDOMOptions;
6
6
  type JestEnvironmentConfig = ConstructorParameters<typeof TestEnvironment>[0];
7
7
  type EnvironmentContext = ConstructorParameters<typeof TestEnvironment>[1];
8
+ /**
9
+ * Jest environment that puts real headless WebGL 1 and 2 (ANGLE via `@onirenaud/node-webgl`) and a browser-like
10
+ * DOM on the sandbox global. Each test file gets a fresh document and animation-frame queue. Set options under
11
+ * `testEnvironmentOptions`; see {@link WebglNodeOptions}.
12
+ */
8
13
  export default class WebglEnvironment extends TestEnvironment {
9
14
  private readonly added;
10
15
  private readonly animationFrames;
16
+ private readonly canvases;
17
+ /**
18
+ * Creates the environment and installs the DOM; Jest calls this once per test file.
19
+ * @param config - Jest's environment config; the WebGL options are read from `testEnvironmentOptions`.
20
+ * @param context - Jest's environment context.
21
+ */
11
22
  constructor(config: JestEnvironmentConfig, context: EnvironmentContext);
23
+ /** Cancels pending animation frames, disposes canvases the environment created and removes the installed globals. */
12
24
  teardown(): Promise<void>;
13
25
  }
14
26
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;YAQY,mBAAmB,cAAc;KAExC,wBAAwB,6BAA6B;KACrD,qBAAqB,6BAA6B;qBA8IlC,yBAAyB;mBAC3B;mBACA;EAEjB,YAAY,QAAQ,uBAAuB,SAAS;EAqDrC,YAAY"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;YAQY,mBAAmB,cAAc;KAExC,wBAAwB,6BAA6B;KACrD,qBAAqB,6BAA6B;;;;;;qBAmJlC,yBAAyB;mBAC3B;mBACA;mBACA;;;;;;EAOjB,YAAY,QAAQ,uBAAuB,SAAS;;EAwErC,YAAY"}
package/dist/index.mjs CHANGED
@@ -137,9 +137,20 @@ function localFetch(baseDir, nativeFetch) {
137
137
  }
138
138
  };
139
139
  }
140
+ /**
141
+ * Jest environment that puts real headless WebGL 1 and 2 (ANGLE via `@onirenaud/node-webgl`) and a browser-like
142
+ * DOM on the sandbox global. Each test file gets a fresh document and animation-frame queue. Set options under
143
+ * `testEnvironmentOptions`; see {@link WebglNodeOptions}.
144
+ */
140
145
  var WebglEnvironment = class extends TestEnvironment {
141
146
  added = [];
142
147
  animationFrames = /* @__PURE__ */ new Map();
148
+ canvases = /* @__PURE__ */ new Set();
149
+ /**
150
+ * Creates the environment and installs the DOM; Jest calls this once per test file.
151
+ * @param config - Jest's environment config; the WebGL options are read from `testEnvironmentOptions`.
152
+ * @param context - Jest's environment context.
153
+ */
143
154
  constructor(config, context) {
144
155
  const options = config.projectConfig.testEnvironmentOptions;
145
156
  super(environmentConfig(config), context);
@@ -165,6 +176,19 @@ var WebglEnvironment = class extends TestEnvironment {
165
176
  g.window = g;
166
177
  g.self = g;
167
178
  g.document.defaultView = g;
179
+ const track = (value) => {
180
+ if (value instanceof nodeWebGL.Canvas) this.canvases.add(value);
181
+ return value;
182
+ };
183
+ for (const name of ["HTMLCanvasElement", "OffscreenCanvas"]) {
184
+ const Canvas = g[name];
185
+ g[name] = new Proxy(Canvas, { construct: (target, args, newTarget) => track(Reflect.construct(target, args, newTarget)) });
186
+ }
187
+ const document = g.document;
188
+ for (const name of ["createElement", "createElementNS"]) {
189
+ const create = document[name];
190
+ document[name] = (...args) => track(create.apply(document, args));
191
+ }
168
192
  const events = new EventTarget();
169
193
  g.addEventListener = events.addEventListener.bind(events);
170
194
  g.removeEventListener = events.removeEventListener.bind(events);
@@ -187,10 +211,13 @@ var WebglEnvironment = class extends TestEnvironment {
187
211
  const nativeFetch = globalThis.fetch.bind(globalThis);
188
212
  g.fetch = domOptions.fetch === false ? nativeFetch : localFetch(domOptions.baseDir ?? process.cwd(), nativeFetch);
189
213
  }
214
+ /** Cancels pending animation frames, disposes canvases the environment created and removes the installed globals. */
190
215
  async teardown() {
191
216
  const g = this.global;
192
217
  for (const [id, clear] of this.animationFrames) clear(id);
193
218
  this.animationFrames.clear();
219
+ for (const canvas of this.canvases) canvas.dispose();
220
+ this.canvases.clear();
194
221
  for (const key of this.added) delete g[key];
195
222
  await super.teardown();
196
223
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["resolvePath"],"sources":["../src/index.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { extname, resolve as resolvePath } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport * as nodeWebGL from '@onirenaud/node-webgl';\nimport type { InitOptions, InstallDOMOptions } from '@onirenaud/node-webgl';\nimport { TestEnvironment } from 'jest-environment-node';\n\n/** Set under `testEnvironmentOptions` in your Jest config. */\nexport type WebglNodeOptions = InitOptions & InstallDOMOptions;\n\ntype JestEnvironmentConfig = ConstructorParameters<typeof TestEnvironment>[0];\ntype EnvironmentContext = ConstructorParameters<typeof TestEnvironment>[1];\ntype Sandbox = Record<string, unknown>;\n\nconst WEBGL_OPTION_NAMES = new Set([\n 'api',\n 'backend',\n 'baseDir',\n 'devicePixelRatio',\n 'fetch',\n 'frameInterval',\n 'innerHeight',\n 'innerWidth',\n]);\n\nconst MIME: Record<string, string> = {\n '.basis': 'application/octet-stream',\n '.bin': 'application/octet-stream',\n '.drc': 'application/octet-stream',\n '.exr': 'image/x-exr',\n '.fbx': 'application/octet-stream',\n '.gif': 'image/gif',\n '.glb': 'model/gltf-binary',\n '.gltf': 'model/gltf+json',\n '.hdr': 'image/vnd.radiance',\n '.jpeg': 'image/jpeg',\n '.jpg': 'image/jpeg',\n '.js': 'text/javascript',\n '.json': 'application/json',\n '.ktx': 'image/ktx',\n '.ktx2': 'image/ktx2',\n '.mjs': 'text/javascript',\n '.mtl': 'text/plain',\n '.obj': 'text/plain',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n '.txt': 'text/plain',\n '.wasm': 'application/wasm',\n '.webp': 'image/webp',\n};\n\nconst DOM_NAMES = [\n 'window',\n 'self',\n 'HTMLCanvasElement',\n 'OffscreenCanvas',\n 'HTMLImageElement',\n 'Image',\n 'ImageData',\n 'ImageBitmap',\n 'createImageBitmap',\n 'FileReader',\n 'HTMLElement',\n 'Element',\n 'Node',\n 'devicePixelRatio',\n 'innerWidth',\n 'innerHeight',\n 'screen',\n 'requestAnimationFrame',\n 'cancelAnimationFrame',\n 'getComputedStyle',\n 'matchMedia',\n 'scrollTo',\n 'alert',\n 'location',\n 'document',\n ...Object.keys(nodeWebGL).filter((name) => name.startsWith('WebGL')),\n] as const;\n\nlet initializedWith: InitOptions | undefined;\n\n/**\n * The native EGL display is process-wide and node-webgl's `init()` is a no-op once it exists (the first\n * context creates it lazily), so compare explicit options against the live display rather than only\n * against what this module itself initialized.\n */\nfunction initialize({ api, backend }: InitOptions): void {\n if (!api && !backend) return;\n if (initializedWith && initializedWith.api === api && initializedWith.backend === backend) return;\n const display = nodeWebGL.getDisplayInfo();\n const mismatch =\n display &&\n ((backend && backend !== 'default' && backend !== display.backend) ||\n (api && api !== 'auto' && api !== display.api));\n if (mismatch) {\n throw new Error(\n `node-webgl is already initialized with ${JSON.stringify({ api: display.api, backend: display.backend })}; ` +\n `cannot switch this Jest worker to ${JSON.stringify({ api, backend })}`,\n );\n }\n nodeWebGL.init({ api, backend });\n initializedWith = { api, backend };\n}\n\n/** Capture a fresh node-webgl DOM while restoring every host descriptor after installation. */\nfunction createDOM(options: InstallDOMOptions): Map<string, PropertyDescriptor> {\n const before = Object.getOwnPropertyDescriptors(globalThis);\n for (const name of DOM_NAMES) Reflect.deleteProperty(globalThis, name);\n\n try {\n nodeWebGL.installDOM({ ...options, fetch: false });\n const after = Object.getOwnPropertyDescriptors(globalThis);\n return new Map(\n DOM_NAMES.flatMap((name): [string, PropertyDescriptor][] => (after[name] ? [[name, after[name]]] : [])),\n );\n } finally {\n for (const name of Object.getOwnPropertyNames(globalThis)) {\n if (!(name in before)) Reflect.deleteProperty(globalThis, name);\n }\n Object.defineProperties(globalThis, before);\n }\n}\n\nfunction environmentConfig(config: JestEnvironmentConfig): JestEnvironmentConfig {\n return {\n ...config,\n projectConfig: {\n ...config.projectConfig,\n testEnvironmentOptions: Object.fromEntries(\n Object.entries(config.projectConfig.testEnvironmentOptions).filter(([name]) => !WEBGL_OPTION_NAMES.has(name)),\n ),\n },\n };\n}\n\nfunction localFetch(baseDir: string, nativeFetch: typeof fetch): typeof fetch {\n return async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {\n const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;\n if (/^(https?|data|blob):/i.test(url)) return nativeFetch(input, init);\n const path = url.startsWith('file:') ? fileURLToPath(url) : resolvePath(baseDir, url.replace(/[?#].*$/, ''));\n try {\n const bytes = await readFile(path);\n return new Response(bytes, {\n status: 200,\n headers: { 'content-type': MIME[extname(path).toLowerCase()] ?? 'application/octet-stream' },\n });\n } catch {\n return new Response(null, { status: 404, statusText: 'Not Found' });\n }\n };\n}\n\nexport default class WebglEnvironment extends TestEnvironment {\n private readonly added: string[] = [];\n private readonly animationFrames = new Map<ReturnType<typeof setTimeout>, typeof clearTimeout>();\n\n constructor(config: JestEnvironmentConfig, context: EnvironmentContext) {\n const options = config.projectConfig.testEnvironmentOptions as WebglNodeOptions;\n super(environmentConfig(config), context);\n\n const { api, backend, ...domOptions } = options;\n initialize({ api, backend });\n\n const g = this.global as unknown as Sandbox;\n const nativeAdded = !Object.getOwnPropertyNames(g).includes('_native');\n if (nativeAdded) {\n Object.defineProperty(g, '_native', {\n value: undefined,\n writable: true,\n configurable: true,\n enumerable: false,\n });\n this.added.push('_native');\n }\n\n for (const [name, descriptor] of createDOM(domOptions)) {\n if (!(name in g)) this.added.push(name);\n Object.defineProperty(g, name, descriptor);\n }\n\n g.window = g;\n g.self = g;\n (g.document as Record<string, unknown>).defaultView = g;\n\n const events = new EventTarget();\n g.addEventListener = events.addEventListener.bind(events);\n g.removeEventListener = events.removeEventListener.bind(events);\n g.dispatchEvent = events.dispatchEvent.bind(events);\n this.added.push('addEventListener', 'removeEventListener', 'dispatchEvent');\n\n const interval = domOptions.frameInterval ?? 16;\n g.requestAnimationFrame = (callback: (time: number) => void) => {\n const id = (g.setTimeout as typeof setTimeout)(() => {\n this.animationFrames.delete(id);\n callback((g.performance as { now(): number }).now());\n }, interval);\n this.animationFrames.set(id, g.clearTimeout as typeof clearTimeout);\n return id;\n };\n g.cancelAnimationFrame = (id: ReturnType<typeof setTimeout>) => {\n const clear = this.animationFrames.get(id);\n this.animationFrames.delete(id);\n (clear ?? (g.clearTimeout as typeof clearTimeout))(id);\n };\n\n const nativeFetch = globalThis.fetch.bind(globalThis);\n g.fetch = domOptions.fetch === false ? nativeFetch : localFetch(domOptions.baseDir ?? process.cwd(), nativeFetch);\n }\n\n override async teardown(): Promise<void> {\n const g = this.global as unknown as Sandbox;\n for (const [id, clear] of this.animationFrames) clear(id);\n this.animationFrames.clear();\n for (const key of this.added) delete g[key];\n await super.teardown();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["resolvePath"],"sources":["../src/index.ts"],"sourcesContent":["import { readFile } from 'node:fs/promises';\nimport { extname, resolve as resolvePath } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport * as nodeWebGL from '@onirenaud/node-webgl';\nimport type { InitOptions, InstallDOMOptions } from '@onirenaud/node-webgl';\nimport { TestEnvironment } from 'jest-environment-node';\n\n/** Set under `testEnvironmentOptions` in your Jest config. */\nexport type WebglNodeOptions = InitOptions & InstallDOMOptions;\n\ntype JestEnvironmentConfig = ConstructorParameters<typeof TestEnvironment>[0];\ntype EnvironmentContext = ConstructorParameters<typeof TestEnvironment>[1];\ntype Sandbox = Record<string, unknown>;\n\nconst WEBGL_OPTION_NAMES = new Set([\n 'api',\n 'backend',\n 'baseDir',\n 'devicePixelRatio',\n 'fetch',\n 'frameInterval',\n 'innerHeight',\n 'innerWidth',\n]);\n\nconst MIME: Record<string, string> = {\n '.basis': 'application/octet-stream',\n '.bin': 'application/octet-stream',\n '.drc': 'application/octet-stream',\n '.exr': 'image/x-exr',\n '.fbx': 'application/octet-stream',\n '.gif': 'image/gif',\n '.glb': 'model/gltf-binary',\n '.gltf': 'model/gltf+json',\n '.hdr': 'image/vnd.radiance',\n '.jpeg': 'image/jpeg',\n '.jpg': 'image/jpeg',\n '.js': 'text/javascript',\n '.json': 'application/json',\n '.ktx': 'image/ktx',\n '.ktx2': 'image/ktx2',\n '.mjs': 'text/javascript',\n '.mtl': 'text/plain',\n '.obj': 'text/plain',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n '.txt': 'text/plain',\n '.wasm': 'application/wasm',\n '.webp': 'image/webp',\n};\n\nconst DOM_NAMES = [\n 'window',\n 'self',\n 'HTMLCanvasElement',\n 'OffscreenCanvas',\n 'HTMLImageElement',\n 'Image',\n 'ImageData',\n 'ImageBitmap',\n 'createImageBitmap',\n 'FileReader',\n 'HTMLElement',\n 'Element',\n 'Node',\n 'devicePixelRatio',\n 'innerWidth',\n 'innerHeight',\n 'screen',\n 'requestAnimationFrame',\n 'cancelAnimationFrame',\n 'getComputedStyle',\n 'matchMedia',\n 'scrollTo',\n 'alert',\n 'location',\n 'document',\n ...Object.keys(nodeWebGL).filter((name) => name.startsWith('WebGL')),\n] as const;\n\nlet initializedWith: InitOptions | undefined;\n\n/**\n * The native EGL display is process-wide and node-webgl's `init()` is a no-op once it exists (the first\n * context creates it lazily), so compare explicit options against the live display rather than only\n * against what this module itself initialized.\n */\nfunction initialize({ api, backend }: InitOptions): void {\n if (!api && !backend) return;\n if (initializedWith && initializedWith.api === api && initializedWith.backend === backend) return;\n const display = nodeWebGL.getDisplayInfo();\n const mismatch =\n display &&\n ((backend && backend !== 'default' && backend !== display.backend) ||\n (api && api !== 'auto' && api !== display.api));\n if (mismatch) {\n throw new Error(\n `node-webgl is already initialized with ${JSON.stringify({ api: display.api, backend: display.backend })}; ` +\n `cannot switch this Jest worker to ${JSON.stringify({ api, backend })}`,\n );\n }\n nodeWebGL.init({ api, backend });\n initializedWith = { api, backend };\n}\n\n/** Capture a fresh node-webgl DOM while restoring every host descriptor after installation. */\nfunction createDOM(options: InstallDOMOptions): Map<string, PropertyDescriptor> {\n const before = Object.getOwnPropertyDescriptors(globalThis);\n for (const name of DOM_NAMES) Reflect.deleteProperty(globalThis, name);\n\n try {\n nodeWebGL.installDOM({ ...options, fetch: false });\n const after = Object.getOwnPropertyDescriptors(globalThis);\n return new Map(\n DOM_NAMES.flatMap((name): [string, PropertyDescriptor][] => (after[name] ? [[name, after[name]]] : [])),\n );\n } finally {\n for (const name of Object.getOwnPropertyNames(globalThis)) {\n if (!(name in before)) Reflect.deleteProperty(globalThis, name);\n }\n Object.defineProperties(globalThis, before);\n }\n}\n\nfunction environmentConfig(config: JestEnvironmentConfig): JestEnvironmentConfig {\n return {\n ...config,\n projectConfig: {\n ...config.projectConfig,\n testEnvironmentOptions: Object.fromEntries(\n Object.entries(config.projectConfig.testEnvironmentOptions).filter(([name]) => !WEBGL_OPTION_NAMES.has(name)),\n ),\n },\n };\n}\n\nfunction localFetch(baseDir: string, nativeFetch: typeof fetch): typeof fetch {\n return async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {\n const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;\n if (/^(https?|data|blob):/i.test(url)) return nativeFetch(input, init);\n const path = url.startsWith('file:') ? fileURLToPath(url) : resolvePath(baseDir, url.replace(/[?#].*$/, ''));\n try {\n const bytes = await readFile(path);\n return new Response(bytes, {\n status: 200,\n headers: { 'content-type': MIME[extname(path).toLowerCase()] ?? 'application/octet-stream' },\n });\n } catch {\n return new Response(null, { status: 404, statusText: 'Not Found' });\n }\n };\n}\n\n/**\n * Jest environment that puts real headless WebGL 1 and 2 (ANGLE via `@onirenaud/node-webgl`) and a browser-like\n * DOM on the sandbox global. Each test file gets a fresh document and animation-frame queue. Set options under\n * `testEnvironmentOptions`; see {@link WebglNodeOptions}.\n */\nexport default class WebglEnvironment extends TestEnvironment {\n private readonly added: string[] = [];\n private readonly animationFrames = new Map<ReturnType<typeof setTimeout>, typeof clearTimeout>();\n private readonly canvases = new Set<nodeWebGL.Canvas>();\n\n /**\n * Creates the environment and installs the DOM; Jest calls this once per test file.\n * @param config - Jest's environment config; the WebGL options are read from `testEnvironmentOptions`.\n * @param context - Jest's environment context.\n */\n constructor(config: JestEnvironmentConfig, context: EnvironmentContext) {\n const options = config.projectConfig.testEnvironmentOptions as WebglNodeOptions;\n super(environmentConfig(config), context);\n\n const { api, backend, ...domOptions } = options;\n initialize({ api, backend });\n\n const g = this.global as unknown as Sandbox;\n const nativeAdded = !Object.getOwnPropertyNames(g).includes('_native');\n if (nativeAdded) {\n Object.defineProperty(g, '_native', {\n value: undefined,\n writable: true,\n configurable: true,\n enumerable: false,\n });\n this.added.push('_native');\n }\n\n for (const [name, descriptor] of createDOM(domOptions)) {\n if (!(name in g)) this.added.push(name);\n Object.defineProperty(g, name, descriptor);\n }\n\n g.window = g;\n g.self = g;\n (g.document as Record<string, unknown>).defaultView = g;\n\n // Native contexts are held in node-webgl's process-wide registry, without a GC finalizer.\n // Keep environment-created canvases alive until we can explicitly release their contexts.\n const track = <T>(value: T): T => {\n if (value instanceof nodeWebGL.Canvas) this.canvases.add(value);\n return value;\n };\n for (const name of ['HTMLCanvasElement', 'OffscreenCanvas'] as const) {\n const Canvas = g[name] as typeof nodeWebGL.Canvas;\n g[name] = new Proxy(Canvas, {\n construct: (target, args, newTarget) => track(Reflect.construct(target, args, newTarget)),\n });\n }\n const document = g.document as Record<'createElement' | 'createElementNS', (...args: unknown[]) => unknown>;\n for (const name of ['createElement', 'createElementNS'] as const) {\n const create = document[name];\n document[name] = (...args: unknown[]) => track(create.apply(document, args));\n }\n\n const events = new EventTarget();\n g.addEventListener = events.addEventListener.bind(events);\n g.removeEventListener = events.removeEventListener.bind(events);\n g.dispatchEvent = events.dispatchEvent.bind(events);\n this.added.push('addEventListener', 'removeEventListener', 'dispatchEvent');\n\n const interval = domOptions.frameInterval ?? 16;\n g.requestAnimationFrame = (callback: (time: number) => void) => {\n const id = (g.setTimeout as typeof setTimeout)(() => {\n this.animationFrames.delete(id);\n callback((g.performance as { now(): number }).now());\n }, interval);\n this.animationFrames.set(id, g.clearTimeout as typeof clearTimeout);\n return id;\n };\n g.cancelAnimationFrame = (id: ReturnType<typeof setTimeout>) => {\n const clear = this.animationFrames.get(id);\n this.animationFrames.delete(id);\n (clear ?? (g.clearTimeout as typeof clearTimeout))(id);\n };\n\n const nativeFetch = globalThis.fetch.bind(globalThis);\n g.fetch = domOptions.fetch === false ? nativeFetch : localFetch(domOptions.baseDir ?? process.cwd(), nativeFetch);\n }\n\n /** Cancels pending animation frames, disposes canvases the environment created and removes the installed globals. */\n override async teardown(): Promise<void> {\n const g = this.global as unknown as Sandbox;\n for (const [id, clear] of this.animationFrames) clear(id);\n this.animationFrames.clear();\n for (const canvas of this.canvases) canvas.dispose();\n this.canvases.clear();\n for (const key of this.added) delete g[key];\n await super.teardown();\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jest-environment-webgl-node",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Jest environment that provides real headless WebGL 1/2 (ANGLE via @onirenaud/node-webgl) in Node",
5
5
  "keywords": [
6
6
  "headless",
@@ -31,8 +31,7 @@
31
31
  "dist/**/*.d.cts",
32
32
  "dist/**/*.d.cts.map",
33
33
  "dist/**/*.d.mts",
34
- "dist/**/*.d.mts.map",
35
- "CHANGELOG.md"
34
+ "dist/**/*.d.mts.map"
36
35
  ],
37
36
  "type": "module",
38
37
  "main": "./dist/index.cjs",
@@ -56,10 +55,6 @@
56
55
  "publishConfig": {
57
56
  "access": "public"
58
57
  },
59
- "scripts": {
60
- "build": "tsdown",
61
- "tsc": "tsc --noEmit"
62
- },
63
58
  "dependencies": {
64
59
  "@onirenaud/node-webgl": "^0.2.0"
65
60
  },
@@ -68,6 +63,10 @@
68
63
  "jest-environment-node": ">=29 <31"
69
64
  },
70
65
  "engines": {
71
- "node": ">=22.12.0"
66
+ "node": ">=22"
67
+ },
68
+ "scripts": {
69
+ "build": "tsdown",
70
+ "tsc": "tsc --noEmit"
72
71
  }
73
- }
72
+ }
package/CHANGELOG.md DELETED
@@ -1,5 +0,0 @@
1
- ## [jest-environment-webgl-node-v0.1.2](https://github.com/bhouston/jest-gpu/compare/jest-environment-webgl-node-v0.1.1...jest-environment-webgl-node-v0.1.2) (2026-09-21)
2
-
3
- ### Bug Fixes
4
-
5
- * **webgl:** detect backend conflicts against the initialized display ([e68573d](https://github.com/bhouston/jest-gpu/commit/e68573d1f5cd20e571d4e313ad244f6a8a41d513)), closes [#21](https://github.com/bhouston/jest-gpu/issues/21)