jest-environment-webgl-node 0.1.1 → 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 +41 -12
- package/dist/index.cjs +51 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +12 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +51 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -9
- package/CHANGELOG.md +0 -5
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
|
|
4
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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,13 +67,19 @@ 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.
|
|
54
78
|
|
|
55
|
-
The native WebGL display itself is process-wide
|
|
56
|
-
projects that can share a Jest worker
|
|
57
|
-
|
|
79
|
+
The native WebGL display itself is process-wide and is created by the first context in a worker, so
|
|
80
|
+
set `backend` and `api` consistently across all projects that can share a Jest worker. The
|
|
81
|
+
environment throws if an explicit `backend` or `api` disagrees with the display already initialized
|
|
82
|
+
in that worker, including one initialized with platform defaults by a project that set no options.
|
|
58
83
|
|
|
59
84
|
Types for the installed globals: add `import 'jest-environment-webgl-node/globals';` to a `.d.ts`
|
|
60
85
|
file your tsconfig includes.
|
|
@@ -68,14 +93,18 @@ On Linux CI install Mesa (`apt-get install libegl1 libgles2 libgl1-mesa-dri`) an
|
|
|
68
93
|
|
|
69
94
|
## Using Vitest?
|
|
70
95
|
|
|
71
|
-
The equivalent package for Vitest is
|
|
96
|
+
The equivalent package for Vitest (see the [vitest-gpu docs](https://vitest-gpu.ben3d.ca/)) is
|
|
72
97
|
[`vitest-environment-webgl-node`](https://github.com/bhouston/vitest-gpu/tree/main/packages/vitest-environment-webgl-node)
|
|
73
98
|
from [vitest-gpu](https://github.com/bhouston/vitest-gpu).
|
|
74
99
|
|
|
75
|
-
##
|
|
100
|
+
## Contribution
|
|
76
101
|
|
|
77
|
-
|
|
102
|
+
See [Contributing Guide](https://github.com/bhouston/jest-gpu/blob/main/CONTRIBUTING.md).
|
|
78
103
|
|
|
79
104
|
## License
|
|
80
105
|
|
|
81
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
|
@@ -95,11 +95,30 @@ const DOM_NAMES = [
|
|
|
95
95
|
...Object.keys(_onirenaud_node_webgl).filter((name) => name.startsWith("WebGL"))
|
|
96
96
|
];
|
|
97
97
|
let initializedWith;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
/**
|
|
99
|
+
* The native EGL display is process-wide and node-webgl's `init()` is a no-op once it exists (the first
|
|
100
|
+
* context creates it lazily), so compare explicit options against the live display rather than only
|
|
101
|
+
* against what this module itself initialized.
|
|
102
|
+
*/
|
|
103
|
+
function initialize({ api, backend }) {
|
|
104
|
+
if (!api && !backend) return;
|
|
105
|
+
if (initializedWith && initializedWith.api === api && initializedWith.backend === backend) return;
|
|
106
|
+
const display = _onirenaud_node_webgl.getDisplayInfo();
|
|
107
|
+
if (display && (backend && backend !== "default" && backend !== display.backend || api && api !== "auto" && api !== display.api)) throw new Error(`node-webgl is already initialized with ${JSON.stringify({
|
|
108
|
+
api: display.api,
|
|
109
|
+
backend: display.backend
|
|
110
|
+
})}; cannot switch this Jest worker to ${JSON.stringify({
|
|
111
|
+
api,
|
|
112
|
+
backend
|
|
113
|
+
})}`);
|
|
114
|
+
_onirenaud_node_webgl.init({
|
|
115
|
+
api,
|
|
116
|
+
backend
|
|
117
|
+
});
|
|
118
|
+
initializedWith = {
|
|
119
|
+
api,
|
|
120
|
+
backend
|
|
121
|
+
};
|
|
103
122
|
}
|
|
104
123
|
/** Capture a fresh node-webgl DOM while restoring every host descriptor after installation. */
|
|
105
124
|
function createDOM(options) {
|
|
@@ -145,9 +164,20 @@ function localFetch(baseDir, nativeFetch) {
|
|
|
145
164
|
}
|
|
146
165
|
};
|
|
147
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
|
+
*/
|
|
148
172
|
var WebglEnvironment = class extends jest_environment_node.TestEnvironment {
|
|
149
173
|
added = [];
|
|
150
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
|
+
*/
|
|
151
181
|
constructor(config, context) {
|
|
152
182
|
const options = config.projectConfig.testEnvironmentOptions;
|
|
153
183
|
super(environmentConfig(config), context);
|
|
@@ -173,6 +203,19 @@ var WebglEnvironment = class extends jest_environment_node.TestEnvironment {
|
|
|
173
203
|
g.window = g;
|
|
174
204
|
g.self = g;
|
|
175
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
|
+
}
|
|
176
219
|
const events = new EventTarget();
|
|
177
220
|
g.addEventListener = events.addEventListener.bind(events);
|
|
178
221
|
g.removeEventListener = events.removeEventListener.bind(events);
|
|
@@ -195,10 +238,13 @@ var WebglEnvironment = class extends jest_environment_node.TestEnvironment {
|
|
|
195
238
|
const nativeFetch = globalThis.fetch.bind(globalThis);
|
|
196
239
|
g.fetch = domOptions.fetch === false ? nativeFetch : localFetch(domOptions.baseDir ?? process.cwd(), nativeFetch);
|
|
197
240
|
}
|
|
241
|
+
/** Cancels pending animation frames, disposes canvases the environment created and removes the installed globals. */
|
|
198
242
|
async teardown() {
|
|
199
243
|
const g = this.global;
|
|
200
244
|
for (const [id, clear] of this.animationFrames) clear(id);
|
|
201
245
|
this.animationFrames.clear();
|
|
246
|
+
for (const canvas of this.canvases) canvas.dispose();
|
|
247
|
+
this.canvases.clear();
|
|
202
248
|
for (const key of this.added) delete g[key];
|
|
203
249
|
await super.teardown();
|
|
204
250
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -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\nfunction initialize(options: InitOptions): void {\n if (!options.api && !options.backend) return;\n if (initializedWith && (options.api !== initializedWith.api || options.backend !== initializedWith.backend)) {\n throw new Error(\n `node-webgl is already initialized with ${JSON.stringify(initializedWith)}; ` +\n `cannot switch this Jest worker to ${JSON.stringify(options)}`,\n );\n }\n nodeWebGL.init(options);\n initializedWith = options;\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
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/index.ts"],"mappings":";;;;YAQY,mBAAmB,cAAc;KAExC,wBAAwB,6BAA6B;KACrD,qBAAqB,6BAA6B
|
|
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
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;YAQY,mBAAmB,cAAc;KAExC,wBAAwB,6BAA6B;KACrD,qBAAqB,6BAA6B
|
|
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
|
@@ -68,11 +68,30 @@ const DOM_NAMES = [
|
|
|
68
68
|
...Object.keys(nodeWebGL).filter((name) => name.startsWith("WebGL"))
|
|
69
69
|
];
|
|
70
70
|
let initializedWith;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
/**
|
|
72
|
+
* The native EGL display is process-wide and node-webgl's `init()` is a no-op once it exists (the first
|
|
73
|
+
* context creates it lazily), so compare explicit options against the live display rather than only
|
|
74
|
+
* against what this module itself initialized.
|
|
75
|
+
*/
|
|
76
|
+
function initialize({ api, backend }) {
|
|
77
|
+
if (!api && !backend) return;
|
|
78
|
+
if (initializedWith && initializedWith.api === api && initializedWith.backend === backend) return;
|
|
79
|
+
const display = nodeWebGL.getDisplayInfo();
|
|
80
|
+
if (display && (backend && backend !== "default" && backend !== display.backend || api && api !== "auto" && api !== display.api)) throw new Error(`node-webgl is already initialized with ${JSON.stringify({
|
|
81
|
+
api: display.api,
|
|
82
|
+
backend: display.backend
|
|
83
|
+
})}; cannot switch this Jest worker to ${JSON.stringify({
|
|
84
|
+
api,
|
|
85
|
+
backend
|
|
86
|
+
})}`);
|
|
87
|
+
nodeWebGL.init({
|
|
88
|
+
api,
|
|
89
|
+
backend
|
|
90
|
+
});
|
|
91
|
+
initializedWith = {
|
|
92
|
+
api,
|
|
93
|
+
backend
|
|
94
|
+
};
|
|
76
95
|
}
|
|
77
96
|
/** Capture a fresh node-webgl DOM while restoring every host descriptor after installation. */
|
|
78
97
|
function createDOM(options) {
|
|
@@ -118,9 +137,20 @@ function localFetch(baseDir, nativeFetch) {
|
|
|
118
137
|
}
|
|
119
138
|
};
|
|
120
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
|
+
*/
|
|
121
145
|
var WebglEnvironment = class extends TestEnvironment {
|
|
122
146
|
added = [];
|
|
123
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
|
+
*/
|
|
124
154
|
constructor(config, context) {
|
|
125
155
|
const options = config.projectConfig.testEnvironmentOptions;
|
|
126
156
|
super(environmentConfig(config), context);
|
|
@@ -146,6 +176,19 @@ var WebglEnvironment = class extends TestEnvironment {
|
|
|
146
176
|
g.window = g;
|
|
147
177
|
g.self = g;
|
|
148
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
|
+
}
|
|
149
192
|
const events = new EventTarget();
|
|
150
193
|
g.addEventListener = events.addEventListener.bind(events);
|
|
151
194
|
g.removeEventListener = events.removeEventListener.bind(events);
|
|
@@ -168,10 +211,13 @@ var WebglEnvironment = class extends TestEnvironment {
|
|
|
168
211
|
const nativeFetch = globalThis.fetch.bind(globalThis);
|
|
169
212
|
g.fetch = domOptions.fetch === false ? nativeFetch : localFetch(domOptions.baseDir ?? process.cwd(), nativeFetch);
|
|
170
213
|
}
|
|
214
|
+
/** Cancels pending animation frames, disposes canvases the environment created and removes the installed globals. */
|
|
171
215
|
async teardown() {
|
|
172
216
|
const g = this.global;
|
|
173
217
|
for (const [id, clear] of this.animationFrames) clear(id);
|
|
174
218
|
this.animationFrames.clear();
|
|
219
|
+
for (const canvas of this.canvases) canvas.dispose();
|
|
220
|
+
this.canvases.clear();
|
|
175
221
|
for (const key of this.added) delete g[key];
|
|
176
222
|
await super.teardown();
|
|
177
223
|
}
|
package/dist/index.mjs.map
CHANGED
|
@@ -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\nfunction initialize(options: InitOptions): void {\n if (!options.api && !options.backend) return;\n if (initializedWith && (options.api !== initializedWith.api || options.backend !== initializedWith.backend)) {\n throw new Error(\n `node-webgl is already initialized with ${JSON.stringify(initializedWith)}; ` +\n `cannot switch this Jest worker to ${JSON.stringify(options)}`,\n );\n }\n nodeWebGL.init(options);\n initializedWith = options;\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.
|
|
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
|
|
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.1](https://github.com/bhouston/jest-gpu/compare/jest-environment-webgl-node-v0.1.0...jest-environment-webgl-node-v0.1.1) (2026-09-21)
|
|
2
|
-
|
|
3
|
-
### Bug Fixes
|
|
4
|
-
|
|
5
|
-
* **webgl:** isolate each Jest environment ([42e711e](https://github.com/bhouston/jest-gpu/commit/42e711e1cf45da303a7a0b13a38d7d48681f31d2)), closes [#19](https://github.com/bhouston/jest-gpu/issues/19)
|