jest-environment-webgpu-node 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +79 -0
- package/dist/index.cjs +203 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +72 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +72 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +196 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +73 -0
- package/src/globals.d.ts +3 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ben Houston
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# jest-environment-webgpu-node
|
|
2
|
+
|
|
3
|
+
Real, headless WebGPU inside Jest, with no browser. Powered by Google's Dawn through the
|
|
4
|
+
[`webgpu`](https://github.com/dawn-gpu/node-webgpu) npm package.
|
|
5
|
+
|
|
6
|
+
The environment puts `navigator.gpu` and every `GPU*` class and constant (`GPUBufferUsage`,
|
|
7
|
+
`GPUShaderStage`, ...) on the test's global object before each test file and removes them
|
|
8
|
+
afterwards.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
pnpm add -D jest jest-environment-webgpu-node
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
// jest.config.mjs
|
|
20
|
+
export default {
|
|
21
|
+
testEnvironment: 'jest-environment-webgpu-node',
|
|
22
|
+
testEnvironmentOptions: { dawnOptions: ['backend=vulkan'] },
|
|
23
|
+
};
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { expect, it } from '@jest/globals';
|
|
28
|
+
|
|
29
|
+
it('runs a compute shader', async () => {
|
|
30
|
+
const device = await (await navigator.gpu.requestAdapter())!.requestDevice();
|
|
31
|
+
// ...createShaderModule, dispatch, copy to a MAP_READ buffer, mapAsync, assert
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`dawnOptions` are passed straight to Dawn: `backend=<null|d3d11|d3d12|metal|vulkan|opengl|opengles>`,
|
|
36
|
+
`adapter=<name>`, `enable-dawn-features=...`, `disable-dawn-features=...`.
|
|
37
|
+
|
|
38
|
+
## Canvas
|
|
39
|
+
|
|
40
|
+
Dawn has no `<canvas>`, so the environment ships a headless one whose `getContext('webgpu')` returns a
|
|
41
|
+
`GPUCanvasContext` backed by a texture. `document.createElement('canvas')`, `HTMLCanvasElement`, `window`,
|
|
42
|
+
`self` and `requestAnimationFrame` are installed too (only when missing), which is enough for three.js
|
|
43
|
+
`WebGPURenderer`, Babylon Lite and vgpu to run unchanged.
|
|
44
|
+
|
|
45
|
+
`createCanvas` is installed as a global, so CommonJS test files can use it without an import. It is also
|
|
46
|
+
available as a named export for ESM test files:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import * as THREE from 'three/webgpu';
|
|
50
|
+
import { createCanvas } from 'jest-environment-webgpu-node';
|
|
51
|
+
|
|
52
|
+
const canvas = createCanvas(256, 256);
|
|
53
|
+
const renderer = new THREE.WebGPURenderer({ canvas: canvas.asElement() });
|
|
54
|
+
await renderer.init();
|
|
55
|
+
await renderer.renderAsync(scene, camera);
|
|
56
|
+
const { width, height, data } = await canvas.readPixels();
|
|
57
|
+
// data is RGBA8, top row first - feed it to a screenshot matcher such as jest-image-snapshot
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`readPixels()` returns `{ width, height, data }` RGBA8 pixels. `asElement()` is the same object typed as an
|
|
61
|
+
`HTMLCanvasElement` for library signatures that demand one.
|
|
62
|
+
|
|
63
|
+
Import `jest-environment-webgpu-node/globals` in a `.d.ts` (or add it to `types` in `tsconfig.json`) to get
|
|
64
|
+
TypeScript types for the global `createCanvas`.
|
|
65
|
+
|
|
66
|
+
## ESM and CommonJS
|
|
67
|
+
|
|
68
|
+
This package ships both an ESM and a CommonJS build, so `require('jest-environment-webgpu-node')`
|
|
69
|
+
works from a plain CommonJS test file, and `testEnvironment: 'jest-environment-webgpu-node'`
|
|
70
|
+
resolves the same way either way. `NODE_OPTIONS=--experimental-vm-modules` is only needed when a
|
|
71
|
+
test file itself imports an ESM-only library, such as `three/webgpu`.
|
|
72
|
+
|
|
73
|
+
## Author
|
|
74
|
+
|
|
75
|
+
Created by [Ben Houston](https://ben3d.ca) and sponsored by [Land of Assets](https://landofassets.com).
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Object.defineProperties(exports, {
|
|
2
|
+
__esModule: { value: true },
|
|
3
|
+
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
+
});
|
|
5
|
+
let jest_environment_node = require("jest-environment-node");
|
|
6
|
+
//#region src/canvas.ts
|
|
7
|
+
const RENDER_ATTACHMENT = 16;
|
|
8
|
+
const MAP_READ = 1;
|
|
9
|
+
/** Minimal `GPUCanvasContext`: `getCurrentTexture()` is a texture the size of the canvas, kept until reconfigured or resized. */
|
|
10
|
+
var HeadlessCanvasContext = class {
|
|
11
|
+
canvas;
|
|
12
|
+
#config;
|
|
13
|
+
#texture;
|
|
14
|
+
constructor(canvas) {
|
|
15
|
+
this.canvas = canvas;
|
|
16
|
+
}
|
|
17
|
+
configure(config) {
|
|
18
|
+
this.#config = config;
|
|
19
|
+
this.#drop();
|
|
20
|
+
}
|
|
21
|
+
unconfigure() {
|
|
22
|
+
this.#config = void 0;
|
|
23
|
+
this.#drop();
|
|
24
|
+
}
|
|
25
|
+
getConfiguration() {
|
|
26
|
+
return this.#config ?? null;
|
|
27
|
+
}
|
|
28
|
+
getCurrentTexture() {
|
|
29
|
+
const config = this.#config;
|
|
30
|
+
if (!config) throw new Error("getCurrentTexture() called before configure()");
|
|
31
|
+
const { width, height } = this.canvas;
|
|
32
|
+
if (this.#texture && (this.#texture.width !== width || this.#texture.height !== height)) this.#drop();
|
|
33
|
+
this.#texture ??= config.device.createTexture({
|
|
34
|
+
size: [width, height],
|
|
35
|
+
format: config.format,
|
|
36
|
+
usage: (config.usage ?? RENDER_ATTACHMENT) | 17,
|
|
37
|
+
viewFormats: config.viewFormats
|
|
38
|
+
});
|
|
39
|
+
return this.#texture;
|
|
40
|
+
}
|
|
41
|
+
/** Copies the current texture to the CPU. BGRA formats are swizzled to RGBA. */
|
|
42
|
+
async readPixels() {
|
|
43
|
+
const texture = this.getCurrentTexture();
|
|
44
|
+
const { device, format } = this.#config;
|
|
45
|
+
const { width, height } = texture;
|
|
46
|
+
const bytesPerRow = Math.ceil(width * 4 / 256) * 256;
|
|
47
|
+
const buffer = device.createBuffer({
|
|
48
|
+
size: bytesPerRow * height,
|
|
49
|
+
usage: 9
|
|
50
|
+
});
|
|
51
|
+
const encoder = device.createCommandEncoder();
|
|
52
|
+
encoder.copyTextureToBuffer({ texture }, {
|
|
53
|
+
buffer,
|
|
54
|
+
bytesPerRow
|
|
55
|
+
}, [width, height]);
|
|
56
|
+
device.queue.submit([encoder.finish()]);
|
|
57
|
+
await buffer.mapAsync(MAP_READ);
|
|
58
|
+
const padded = new Uint8Array(buffer.getMappedRange());
|
|
59
|
+
const data = new Uint8Array(width * height * 4);
|
|
60
|
+
for (let y = 0; y < height; y++) data.set(padded.subarray(y * bytesPerRow, y * bytesPerRow + width * 4), y * width * 4);
|
|
61
|
+
buffer.unmap();
|
|
62
|
+
buffer.destroy();
|
|
63
|
+
if (format.startsWith("bgra")) for (let i = 0; i < data.length; i += 4) [data[i], data[i + 2]] = [data[i + 2], data[i]];
|
|
64
|
+
return {
|
|
65
|
+
width,
|
|
66
|
+
height,
|
|
67
|
+
data
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
#drop() {
|
|
71
|
+
this.#texture?.destroy();
|
|
72
|
+
this.#texture = void 0;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
/** Enough of `HTMLCanvasElement` for three.js `WebGPURenderer`, Babylon Lite and friends. */
|
|
76
|
+
var HeadlessCanvas = class {
|
|
77
|
+
width;
|
|
78
|
+
height;
|
|
79
|
+
#context = new HeadlessCanvasContext(this);
|
|
80
|
+
style = {};
|
|
81
|
+
#attributes = /* @__PURE__ */ new Map();
|
|
82
|
+
constructor(width = 300, height = 150) {
|
|
83
|
+
this.width = width;
|
|
84
|
+
this.height = height;
|
|
85
|
+
}
|
|
86
|
+
get clientWidth() {
|
|
87
|
+
return this.width;
|
|
88
|
+
}
|
|
89
|
+
get clientHeight() {
|
|
90
|
+
return this.height;
|
|
91
|
+
}
|
|
92
|
+
getContext(id) {
|
|
93
|
+
return id === "webgpu" ? this.#context : null;
|
|
94
|
+
}
|
|
95
|
+
readPixels() {
|
|
96
|
+
return this.#context.readPixels();
|
|
97
|
+
}
|
|
98
|
+
/** This same object, typed as an element for libraries whose signatures demand one. */
|
|
99
|
+
asElement() {
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
getBoundingClientRect() {
|
|
103
|
+
return {
|
|
104
|
+
x: 0,
|
|
105
|
+
y: 0,
|
|
106
|
+
top: 0,
|
|
107
|
+
left: 0,
|
|
108
|
+
width: this.width,
|
|
109
|
+
height: this.height
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
setAttribute(name, value) {
|
|
113
|
+
this.#attributes.set(name, value);
|
|
114
|
+
}
|
|
115
|
+
getAttribute(name) {
|
|
116
|
+
return this.#attributes.get(name) ?? null;
|
|
117
|
+
}
|
|
118
|
+
addEventListener() {}
|
|
119
|
+
removeEventListener() {}
|
|
120
|
+
dispatchEvent() {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const createCanvas = (width, height) => new HeadlessCanvas(width, height);
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region src/index.ts
|
|
127
|
+
/** Just enough DOM for libraries that make their own canvas and drive a frame loop. Only installed when missing. */
|
|
128
|
+
const requestAnimationFrame = (callback) => setTimeout(() => callback(performance.now()), 16).unref();
|
|
129
|
+
const domShims = () => {
|
|
130
|
+
const window = {
|
|
131
|
+
devicePixelRatio: 1,
|
|
132
|
+
requestAnimationFrame,
|
|
133
|
+
cancelAnimationFrame: clearTimeout,
|
|
134
|
+
addEventListener() {},
|
|
135
|
+
removeEventListener() {}
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
HTMLCanvasElement: HeadlessCanvas,
|
|
139
|
+
document: {
|
|
140
|
+
createElement: (tag) => tag === "canvas" ? new HeadlessCanvas() : {},
|
|
141
|
+
createElementNS: (_ns, tag) => tag === "canvas" ? new HeadlessCanvas() : {},
|
|
142
|
+
addEventListener() {},
|
|
143
|
+
removeEventListener() {}
|
|
144
|
+
},
|
|
145
|
+
window,
|
|
146
|
+
self: window,
|
|
147
|
+
requestAnimationFrame,
|
|
148
|
+
cancelAnimationFrame: clearTimeout
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Jest environment that puts real headless WebGPU (Dawn via the `webgpu` package) on the sandbox
|
|
153
|
+
* global: `navigator.gpu`, every `GPU*` class and constant, and a `HeadlessCanvas` shim. CommonJS
|
|
154
|
+
* tests can rely on `createCanvas` being a global; ESM tests can also `import` it from this package.
|
|
155
|
+
*/
|
|
156
|
+
var WebgpuEnvironment = class extends jest_environment_node.TestEnvironment {
|
|
157
|
+
#added = [];
|
|
158
|
+
#navigator = {};
|
|
159
|
+
#nativeAdded;
|
|
160
|
+
#dawnOptions;
|
|
161
|
+
constructor(config, context) {
|
|
162
|
+
super(config, context);
|
|
163
|
+
this.#nativeAdded = !Object.getOwnPropertyNames(this.global).includes("_native");
|
|
164
|
+
if (this.#nativeAdded) Object.defineProperty(this.global, "_native", {
|
|
165
|
+
value: void 0,
|
|
166
|
+
writable: true,
|
|
167
|
+
configurable: true,
|
|
168
|
+
enumerable: false
|
|
169
|
+
});
|
|
170
|
+
this.#dawnOptions = config.projectConfig.testEnvironmentOptions.dawnOptions;
|
|
171
|
+
}
|
|
172
|
+
async setup() {
|
|
173
|
+
await super.setup();
|
|
174
|
+
const { create, globals } = await import("webgpu");
|
|
175
|
+
const global = this.global;
|
|
176
|
+
const shims = {
|
|
177
|
+
...globals,
|
|
178
|
+
...domShims(),
|
|
179
|
+
createCanvas
|
|
180
|
+
};
|
|
181
|
+
this.#added = Object.keys(shims).filter((key) => !(key in global));
|
|
182
|
+
for (const key of this.#added) global[key] = shims[key];
|
|
183
|
+
if (this.#nativeAdded) this.#added.push("_native");
|
|
184
|
+
this.#navigator = global.navigator ??= {};
|
|
185
|
+
Object.defineProperty(this.#navigator, "gpu", {
|
|
186
|
+
value: create(this.#dawnOptions ?? []),
|
|
187
|
+
configurable: true
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
async teardown() {
|
|
191
|
+
delete this.#navigator.gpu;
|
|
192
|
+
const global = this.global;
|
|
193
|
+
for (const key of this.#added) delete global[key];
|
|
194
|
+
await super.teardown();
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
//#endregion
|
|
198
|
+
exports.HeadlessCanvas = HeadlessCanvas;
|
|
199
|
+
exports.HeadlessCanvasContext = HeadlessCanvasContext;
|
|
200
|
+
exports.createCanvas = createCanvas;
|
|
201
|
+
exports.default = WebgpuEnvironment;
|
|
202
|
+
|
|
203
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["TestEnvironment"],"sources":["../src/canvas.ts","../src/index.ts"],"sourcesContent":["/** RGBA8 pixels, top row first. Feed it to a screenshot matcher such as jest-image-snapshot after encoding to PNG. */\nexport type RgbaImage = { width: number; height: number; data: Uint8Array };\n\nconst COPY_SRC = 0x01;\nconst RENDER_ATTACHMENT = 0x10;\nconst MAP_READ = 0x01;\nconst COPY_DST = 0x08;\n\n/** Minimal `GPUCanvasContext`: `getCurrentTexture()` is a texture the size of the canvas, kept until reconfigured or resized. */\nexport class HeadlessCanvasContext {\n #config: GPUCanvasConfiguration | undefined;\n #texture: GPUTexture | undefined;\n\n constructor(readonly canvas: HeadlessCanvas) {}\n\n configure(config: GPUCanvasConfiguration): void {\n this.#config = config;\n this.#drop();\n }\n\n unconfigure(): void {\n this.#config = undefined;\n this.#drop();\n }\n\n getConfiguration(): GPUCanvasConfiguration | null {\n return this.#config ?? null;\n }\n\n getCurrentTexture(): GPUTexture {\n const config = this.#config;\n if (!config) throw new Error('getCurrentTexture() called before configure()');\n const { width, height } = this.canvas;\n if (this.#texture && (this.#texture.width !== width || this.#texture.height !== height)) this.#drop();\n this.#texture ??= config.device.createTexture({\n size: [width, height],\n format: config.format,\n usage: (config.usage ?? RENDER_ATTACHMENT) | RENDER_ATTACHMENT | COPY_SRC,\n viewFormats: config.viewFormats,\n });\n return this.#texture;\n }\n\n /** Copies the current texture to the CPU. BGRA formats are swizzled to RGBA. */\n async readPixels(): Promise<RgbaImage> {\n const texture = this.getCurrentTexture();\n const { device, format } = this.#config!;\n const { width, height } = texture;\n const bytesPerRow = Math.ceil((width * 4) / 256) * 256;\n const buffer = device.createBuffer({ size: bytesPerRow * height, usage: MAP_READ | COPY_DST });\n const encoder = device.createCommandEncoder();\n encoder.copyTextureToBuffer({ texture }, { buffer, bytesPerRow }, [width, height]);\n device.queue.submit([encoder.finish()]);\n await buffer.mapAsync(MAP_READ);\n const padded = new Uint8Array(buffer.getMappedRange());\n const data = new Uint8Array(width * height * 4);\n for (let y = 0; y < height; y++)\n data.set(padded.subarray(y * bytesPerRow, y * bytesPerRow + width * 4), y * width * 4);\n buffer.unmap();\n buffer.destroy();\n if (format.startsWith('bgra')) {\n for (let i = 0; i < data.length; i += 4) [data[i], data[i + 2]] = [data[i + 2]!, data[i]!];\n }\n return { width, height, data };\n }\n\n #drop(): void {\n this.#texture?.destroy();\n this.#texture = undefined;\n }\n}\n\n/** Enough of `HTMLCanvasElement` for three.js `WebGPURenderer`, Babylon Lite and friends. */\nexport class HeadlessCanvas {\n readonly #context = new HeadlessCanvasContext(this);\n readonly style: Record<string, string> = {};\n readonly #attributes = new Map<string, string>();\n\n constructor(\n public width = 300,\n public height = 150,\n ) {}\n\n get clientWidth(): number {\n return this.width;\n }\n\n get clientHeight(): number {\n return this.height;\n }\n\n getContext(id: 'webgpu'): HeadlessCanvasContext;\n getContext(id: string): HeadlessCanvasContext | null;\n getContext(id: string): HeadlessCanvasContext | null {\n return id === 'webgpu' ? this.#context : null;\n }\n\n readPixels(): Promise<RgbaImage> {\n return this.#context.readPixels();\n }\n\n /** This same object, typed as an element for libraries whose signatures demand one. */\n asElement(): HTMLCanvasElement {\n return this as unknown as HTMLCanvasElement;\n }\n\n getBoundingClientRect(): { x: number; y: number; width: number; height: number; top: number; left: number } {\n return { x: 0, y: 0, top: 0, left: 0, width: this.width, height: this.height };\n }\n\n setAttribute(name: string, value: string): void {\n this.#attributes.set(name, value);\n }\n\n getAttribute(name: string): string | null {\n return this.#attributes.get(name) ?? null;\n }\n\n addEventListener(): void {}\n removeEventListener(): void {}\n dispatchEvent(): boolean {\n return true;\n }\n}\n\nexport const createCanvas = (width: number, height: number): HeadlessCanvas => new HeadlessCanvas(width, height);\n","import { TestEnvironment } from 'jest-environment-node';\nimport { createCanvas, HeadlessCanvas } from './canvas.js';\n\ntype JestEnvironmentConfig = ConstructorParameters<typeof TestEnvironment>[0];\ntype EnvironmentContext = ConstructorParameters<typeof TestEnvironment>[1];\n\nexport { createCanvas, HeadlessCanvas, HeadlessCanvasContext, type RgbaImage } from './canvas.js';\n\n/** Set under `testEnvironmentOptions` in your Jest config. */\nexport type WebgpuNodeOptions = {\n /** Dawn options, e.g. `['backend=vulkan', 'enable-dawn-features=allow_unsafe_apis']`. */\n dawnOptions?: string[];\n};\n\n/** Just enough DOM for libraries that make their own canvas and drive a frame loop. Only installed when missing. */\nconst requestAnimationFrame = (callback: (time: number) => void): NodeJS.Timeout =>\n setTimeout(() => callback(performance.now()), 16).unref();\n\nconst domShims = (): Record<string, unknown> => {\n const window = {\n devicePixelRatio: 1,\n requestAnimationFrame,\n cancelAnimationFrame: clearTimeout,\n addEventListener() {},\n removeEventListener() {},\n };\n return {\n HTMLCanvasElement: HeadlessCanvas,\n document: {\n createElement: (tag: string) => (tag === 'canvas' ? new HeadlessCanvas() : {}),\n createElementNS: (_ns: string, tag: string) => (tag === 'canvas' ? new HeadlessCanvas() : {}),\n addEventListener() {},\n removeEventListener() {},\n },\n window,\n self: window,\n requestAnimationFrame,\n cancelAnimationFrame: clearTimeout,\n };\n};\n\n/**\n * Jest environment that puts real headless WebGPU (Dawn via the `webgpu` package) on the sandbox\n * global: `navigator.gpu`, every `GPU*` class and constant, and a `HeadlessCanvas` shim. CommonJS\n * tests can rely on `createCanvas` being a global; ESM tests can also `import` it from this package.\n */\nexport default class WebgpuEnvironment extends TestEnvironment {\n #added: string[] = [];\n #navigator: { gpu?: GPU } = {};\n #nativeAdded: boolean;\n #dawnOptions: string[] | undefined;\n\n constructor(config: JestEnvironmentConfig, context: EnvironmentContext) {\n super(config, context);\n // @babylonjs/core defines a `_native` accessor on `self` at module load; on Jest's proxied\n // sandbox global that defineProperty violates a proxy invariant, so pre-seed the property to\n // satisfy Babylon's hasOwnProperty guard.\n // `Object.hasOwn`/`in` report false negatives here: jest-environment-node's sandbox global is\n // a Proxy over a vm context, and reads of freshly-added properties from outside the sandbox\n // (as this constructor runs) don't go through the fast path those use. getOwnPropertyNames is\n // unaffected, so use that instead.\n this.#nativeAdded = !Object.getOwnPropertyNames(this.global).includes('_native');\n if (this.#nativeAdded) {\n Object.defineProperty(this.global, '_native', {\n value: undefined,\n writable: true,\n configurable: true,\n enumerable: false,\n });\n }\n this.#dawnOptions = config.projectConfig.testEnvironmentOptions.dawnOptions as string[] | undefined;\n }\n\n // `webgpu` is ESM-only. Loading it via a dynamic import here (rather than a static top-level\n // import) keeps it out of this module's own top-level module graph, so a CommonJS test file's\n // `require('jest-environment-webgpu-node')` - which only needs `createCanvas` - never forces\n // Jest's sandboxed CJS loader to load an ESM-only dependency (jestjs/jest#15716). This method\n // itself runs outside that sandbox, through Jest's own environment setup, where Node's native\n // `import()` always works.\n override async setup(): Promise<void> {\n await super.setup();\n const { create, globals } = await import('webgpu');\n const global = this.global as unknown as Record<string, unknown>;\n const shims = { ...globals, ...domShims(), createCanvas } as Record<string, unknown>;\n this.#added = Object.keys(shims).filter((key) => !(key in global));\n for (const key of this.#added) global[key] = shims[key];\n if (this.#nativeAdded) this.#added.push('_native');\n this.#navigator = (global.navigator ??= {}) as { gpu?: GPU };\n Object.defineProperty(this.#navigator, 'gpu', { value: create(this.#dawnOptions ?? []), configurable: true });\n }\n\n override async teardown(): Promise<void> {\n delete this.#navigator.gpu;\n const global = this.global as unknown as Record<string, unknown>;\n for (const key of this.#added) delete global[key];\n await super.teardown();\n }\n}\n"],"mappings":"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"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { TestEnvironment } from "jest-environment-node";
|
|
2
|
+
//#region src/canvas.d.ts
|
|
3
|
+
/** RGBA8 pixels, top row first. Feed it to a screenshot matcher such as jest-image-snapshot after encoding to PNG. */
|
|
4
|
+
type RgbaImage = {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
data: Uint8Array;
|
|
8
|
+
};
|
|
9
|
+
/** Minimal `GPUCanvasContext`: `getCurrentTexture()` is a texture the size of the canvas, kept until reconfigured or resized. */
|
|
10
|
+
export declare class HeadlessCanvasContext {
|
|
11
|
+
#private;
|
|
12
|
+
readonly canvas: HeadlessCanvas;
|
|
13
|
+
constructor(canvas: HeadlessCanvas);
|
|
14
|
+
configure(config: GPUCanvasConfiguration): void;
|
|
15
|
+
unconfigure(): void;
|
|
16
|
+
getConfiguration(): GPUCanvasConfiguration | null;
|
|
17
|
+
getCurrentTexture(): GPUTexture;
|
|
18
|
+
/** Copies the current texture to the CPU. BGRA formats are swizzled to RGBA. */
|
|
19
|
+
readPixels(): Promise<RgbaImage>;
|
|
20
|
+
}
|
|
21
|
+
/** Enough of `HTMLCanvasElement` for three.js `WebGPURenderer`, Babylon Lite and friends. */
|
|
22
|
+
export declare class HeadlessCanvas {
|
|
23
|
+
#private;
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
readonly style: Record<string, string>;
|
|
27
|
+
constructor(width?: number, height?: number);
|
|
28
|
+
get clientWidth(): number;
|
|
29
|
+
get clientHeight(): number;
|
|
30
|
+
getContext(id: 'webgpu'): HeadlessCanvasContext;
|
|
31
|
+
getContext(id: string): HeadlessCanvasContext | null;
|
|
32
|
+
readPixels(): Promise<RgbaImage>;
|
|
33
|
+
/** This same object, typed as an element for libraries whose signatures demand one. */
|
|
34
|
+
asElement(): HTMLCanvasElement;
|
|
35
|
+
getBoundingClientRect(): {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
width: number;
|
|
39
|
+
height: number;
|
|
40
|
+
top: number;
|
|
41
|
+
left: number;
|
|
42
|
+
};
|
|
43
|
+
setAttribute(name: string, value: string): void;
|
|
44
|
+
getAttribute(name: string): string | null;
|
|
45
|
+
addEventListener(): void;
|
|
46
|
+
removeEventListener(): void;
|
|
47
|
+
dispatchEvent(): boolean;
|
|
48
|
+
}
|
|
49
|
+
export declare const createCanvas: (width: number, height: number) => HeadlessCanvas;
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/index.d.ts
|
|
52
|
+
type JestEnvironmentConfig = ConstructorParameters<typeof TestEnvironment>[0];
|
|
53
|
+
type EnvironmentContext = ConstructorParameters<typeof TestEnvironment>[1];
|
|
54
|
+
/** Set under `testEnvironmentOptions` in your Jest config. */
|
|
55
|
+
export type WebgpuNodeOptions = {
|
|
56
|
+
/** Dawn options, e.g. `['backend=vulkan', 'enable-dawn-features=allow_unsafe_apis']`. */
|
|
57
|
+
dawnOptions?: string[];
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Jest environment that puts real headless WebGPU (Dawn via the `webgpu` package) on the sandbox
|
|
61
|
+
* global: `navigator.gpu`, every `GPU*` class and constant, and a `HeadlessCanvas` shim. CommonJS
|
|
62
|
+
* tests can rely on `createCanvas` being a global; ESM tests can also `import` it from this package.
|
|
63
|
+
*/
|
|
64
|
+
export default class WebgpuEnvironment extends TestEnvironment {
|
|
65
|
+
#private;
|
|
66
|
+
constructor(config: JestEnvironmentConfig, context: EnvironmentContext);
|
|
67
|
+
setup(): Promise<void>;
|
|
68
|
+
teardown(): Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
export type { RgbaImage };
|
|
72
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/canvas.ts","../src/index.ts"],"mappings":";;;KACY;EAAc;EAAe;EAAgB,MAAM;;;qBAQlD;;WAIU,QAAQ;EAA7B,YAAqB,QAAQ;EAE7B,UAAU,QAAQ;EAKlB;EAKA,oBAAoB;EAIpB,qBAAqB;;EAef,cAAc,QAAQ;;;qBA6BjB;;EAMF;EACA;WALA,OAAO;EAGhB,YACS,gBACA;MAGL;MAIA;EAIJ,WAAW,eAAe;EAC1B,WAAW,aAAa;EAKxB,cAAc,QAAQ;;EAKtB,aAAa;EAIb;IAA2B;IAAW;IAAW;IAAe;IAAgB;IAAa;;EAI7F,aAAa,cAAc;EAI3B,aAAa;EAIb;EACA;EACA;;qBAKW,eAAY,eAAiB,mBAAmB;;;KC1HxD,wBAAwB,6BAA6B;KACrD,qBAAqB,6BAA6B;;YAK3C;;EAEV;;;;;;;qBAmCmB,0BAA0B;;EAM7C,YAAY,QAAQ,uBAAuB,SAAS;EA2BrC,SAAS;EAYT,YAAY"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { TestEnvironment } from "jest-environment-node";
|
|
2
|
+
//#region src/canvas.d.ts
|
|
3
|
+
/** RGBA8 pixels, top row first. Feed it to a screenshot matcher such as jest-image-snapshot after encoding to PNG. */
|
|
4
|
+
type RgbaImage = {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
data: Uint8Array;
|
|
8
|
+
};
|
|
9
|
+
/** Minimal `GPUCanvasContext`: `getCurrentTexture()` is a texture the size of the canvas, kept until reconfigured or resized. */
|
|
10
|
+
export declare class HeadlessCanvasContext {
|
|
11
|
+
#private;
|
|
12
|
+
readonly canvas: HeadlessCanvas;
|
|
13
|
+
constructor(canvas: HeadlessCanvas);
|
|
14
|
+
configure(config: GPUCanvasConfiguration): void;
|
|
15
|
+
unconfigure(): void;
|
|
16
|
+
getConfiguration(): GPUCanvasConfiguration | null;
|
|
17
|
+
getCurrentTexture(): GPUTexture;
|
|
18
|
+
/** Copies the current texture to the CPU. BGRA formats are swizzled to RGBA. */
|
|
19
|
+
readPixels(): Promise<RgbaImage>;
|
|
20
|
+
}
|
|
21
|
+
/** Enough of `HTMLCanvasElement` for three.js `WebGPURenderer`, Babylon Lite and friends. */
|
|
22
|
+
export declare class HeadlessCanvas {
|
|
23
|
+
#private;
|
|
24
|
+
width: number;
|
|
25
|
+
height: number;
|
|
26
|
+
readonly style: Record<string, string>;
|
|
27
|
+
constructor(width?: number, height?: number);
|
|
28
|
+
get clientWidth(): number;
|
|
29
|
+
get clientHeight(): number;
|
|
30
|
+
getContext(id: 'webgpu'): HeadlessCanvasContext;
|
|
31
|
+
getContext(id: string): HeadlessCanvasContext | null;
|
|
32
|
+
readPixels(): Promise<RgbaImage>;
|
|
33
|
+
/** This same object, typed as an element for libraries whose signatures demand one. */
|
|
34
|
+
asElement(): HTMLCanvasElement;
|
|
35
|
+
getBoundingClientRect(): {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
width: number;
|
|
39
|
+
height: number;
|
|
40
|
+
top: number;
|
|
41
|
+
left: number;
|
|
42
|
+
};
|
|
43
|
+
setAttribute(name: string, value: string): void;
|
|
44
|
+
getAttribute(name: string): string | null;
|
|
45
|
+
addEventListener(): void;
|
|
46
|
+
removeEventListener(): void;
|
|
47
|
+
dispatchEvent(): boolean;
|
|
48
|
+
}
|
|
49
|
+
export declare const createCanvas: (width: number, height: number) => HeadlessCanvas;
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/index.d.ts
|
|
52
|
+
type JestEnvironmentConfig = ConstructorParameters<typeof TestEnvironment>[0];
|
|
53
|
+
type EnvironmentContext = ConstructorParameters<typeof TestEnvironment>[1];
|
|
54
|
+
/** Set under `testEnvironmentOptions` in your Jest config. */
|
|
55
|
+
export type WebgpuNodeOptions = {
|
|
56
|
+
/** Dawn options, e.g. `['backend=vulkan', 'enable-dawn-features=allow_unsafe_apis']`. */
|
|
57
|
+
dawnOptions?: string[];
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Jest environment that puts real headless WebGPU (Dawn via the `webgpu` package) on the sandbox
|
|
61
|
+
* global: `navigator.gpu`, every `GPU*` class and constant, and a `HeadlessCanvas` shim. CommonJS
|
|
62
|
+
* tests can rely on `createCanvas` being a global; ESM tests can also `import` it from this package.
|
|
63
|
+
*/
|
|
64
|
+
export default class WebgpuEnvironment extends TestEnvironment {
|
|
65
|
+
#private;
|
|
66
|
+
constructor(config: JestEnvironmentConfig, context: EnvironmentContext);
|
|
67
|
+
setup(): Promise<void>;
|
|
68
|
+
teardown(): Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
export type { RgbaImage };
|
|
72
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/canvas.ts","../src/index.ts"],"mappings":";;;KACY;EAAc;EAAe;EAAgB,MAAM;;;qBAQlD;;WAIU,QAAQ;EAA7B,YAAqB,QAAQ;EAE7B,UAAU,QAAQ;EAKlB;EAKA,oBAAoB;EAIpB,qBAAqB;;EAef,cAAc,QAAQ;;;qBA6BjB;;EAMF;EACA;WALA,OAAO;EAGhB,YACS,gBACA;MAGL;MAIA;EAIJ,WAAW,eAAe;EAC1B,WAAW,aAAa;EAKxB,cAAc,QAAQ;;EAKtB,aAAa;EAIb;IAA2B;IAAW;IAAW;IAAe;IAAgB;IAAa;;EAI7F,aAAa,cAAc;EAI3B,aAAa;EAIb;EACA;EACA;;qBAKW,eAAY,eAAiB,mBAAmB;;;KC1HxD,wBAAwB,6BAA6B;KACrD,qBAAqB,6BAA6B;;YAK3C;;EAEV;;;;;;;qBAmCmB,0BAA0B;;EAM7C,YAAY,QAAQ,uBAAuB,SAAS;EA2BrC,SAAS;EAYT,YAAY"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { TestEnvironment } from "jest-environment-node";
|
|
2
|
+
//#region src/canvas.ts
|
|
3
|
+
const RENDER_ATTACHMENT = 16;
|
|
4
|
+
const MAP_READ = 1;
|
|
5
|
+
/** Minimal `GPUCanvasContext`: `getCurrentTexture()` is a texture the size of the canvas, kept until reconfigured or resized. */
|
|
6
|
+
var HeadlessCanvasContext = class {
|
|
7
|
+
canvas;
|
|
8
|
+
#config;
|
|
9
|
+
#texture;
|
|
10
|
+
constructor(canvas) {
|
|
11
|
+
this.canvas = canvas;
|
|
12
|
+
}
|
|
13
|
+
configure(config) {
|
|
14
|
+
this.#config = config;
|
|
15
|
+
this.#drop();
|
|
16
|
+
}
|
|
17
|
+
unconfigure() {
|
|
18
|
+
this.#config = void 0;
|
|
19
|
+
this.#drop();
|
|
20
|
+
}
|
|
21
|
+
getConfiguration() {
|
|
22
|
+
return this.#config ?? null;
|
|
23
|
+
}
|
|
24
|
+
getCurrentTexture() {
|
|
25
|
+
const config = this.#config;
|
|
26
|
+
if (!config) throw new Error("getCurrentTexture() called before configure()");
|
|
27
|
+
const { width, height } = this.canvas;
|
|
28
|
+
if (this.#texture && (this.#texture.width !== width || this.#texture.height !== height)) this.#drop();
|
|
29
|
+
this.#texture ??= config.device.createTexture({
|
|
30
|
+
size: [width, height],
|
|
31
|
+
format: config.format,
|
|
32
|
+
usage: (config.usage ?? RENDER_ATTACHMENT) | 17,
|
|
33
|
+
viewFormats: config.viewFormats
|
|
34
|
+
});
|
|
35
|
+
return this.#texture;
|
|
36
|
+
}
|
|
37
|
+
/** Copies the current texture to the CPU. BGRA formats are swizzled to RGBA. */
|
|
38
|
+
async readPixels() {
|
|
39
|
+
const texture = this.getCurrentTexture();
|
|
40
|
+
const { device, format } = this.#config;
|
|
41
|
+
const { width, height } = texture;
|
|
42
|
+
const bytesPerRow = Math.ceil(width * 4 / 256) * 256;
|
|
43
|
+
const buffer = device.createBuffer({
|
|
44
|
+
size: bytesPerRow * height,
|
|
45
|
+
usage: 9
|
|
46
|
+
});
|
|
47
|
+
const encoder = device.createCommandEncoder();
|
|
48
|
+
encoder.copyTextureToBuffer({ texture }, {
|
|
49
|
+
buffer,
|
|
50
|
+
bytesPerRow
|
|
51
|
+
}, [width, height]);
|
|
52
|
+
device.queue.submit([encoder.finish()]);
|
|
53
|
+
await buffer.mapAsync(MAP_READ);
|
|
54
|
+
const padded = new Uint8Array(buffer.getMappedRange());
|
|
55
|
+
const data = new Uint8Array(width * height * 4);
|
|
56
|
+
for (let y = 0; y < height; y++) data.set(padded.subarray(y * bytesPerRow, y * bytesPerRow + width * 4), y * width * 4);
|
|
57
|
+
buffer.unmap();
|
|
58
|
+
buffer.destroy();
|
|
59
|
+
if (format.startsWith("bgra")) for (let i = 0; i < data.length; i += 4) [data[i], data[i + 2]] = [data[i + 2], data[i]];
|
|
60
|
+
return {
|
|
61
|
+
width,
|
|
62
|
+
height,
|
|
63
|
+
data
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
#drop() {
|
|
67
|
+
this.#texture?.destroy();
|
|
68
|
+
this.#texture = void 0;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
/** Enough of `HTMLCanvasElement` for three.js `WebGPURenderer`, Babylon Lite and friends. */
|
|
72
|
+
var HeadlessCanvas = class {
|
|
73
|
+
width;
|
|
74
|
+
height;
|
|
75
|
+
#context = new HeadlessCanvasContext(this);
|
|
76
|
+
style = {};
|
|
77
|
+
#attributes = /* @__PURE__ */ new Map();
|
|
78
|
+
constructor(width = 300, height = 150) {
|
|
79
|
+
this.width = width;
|
|
80
|
+
this.height = height;
|
|
81
|
+
}
|
|
82
|
+
get clientWidth() {
|
|
83
|
+
return this.width;
|
|
84
|
+
}
|
|
85
|
+
get clientHeight() {
|
|
86
|
+
return this.height;
|
|
87
|
+
}
|
|
88
|
+
getContext(id) {
|
|
89
|
+
return id === "webgpu" ? this.#context : null;
|
|
90
|
+
}
|
|
91
|
+
readPixels() {
|
|
92
|
+
return this.#context.readPixels();
|
|
93
|
+
}
|
|
94
|
+
/** This same object, typed as an element for libraries whose signatures demand one. */
|
|
95
|
+
asElement() {
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
getBoundingClientRect() {
|
|
99
|
+
return {
|
|
100
|
+
x: 0,
|
|
101
|
+
y: 0,
|
|
102
|
+
top: 0,
|
|
103
|
+
left: 0,
|
|
104
|
+
width: this.width,
|
|
105
|
+
height: this.height
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
setAttribute(name, value) {
|
|
109
|
+
this.#attributes.set(name, value);
|
|
110
|
+
}
|
|
111
|
+
getAttribute(name) {
|
|
112
|
+
return this.#attributes.get(name) ?? null;
|
|
113
|
+
}
|
|
114
|
+
addEventListener() {}
|
|
115
|
+
removeEventListener() {}
|
|
116
|
+
dispatchEvent() {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const createCanvas = (width, height) => new HeadlessCanvas(width, height);
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region src/index.ts
|
|
123
|
+
/** Just enough DOM for libraries that make their own canvas and drive a frame loop. Only installed when missing. */
|
|
124
|
+
const requestAnimationFrame = (callback) => setTimeout(() => callback(performance.now()), 16).unref();
|
|
125
|
+
const domShims = () => {
|
|
126
|
+
const window = {
|
|
127
|
+
devicePixelRatio: 1,
|
|
128
|
+
requestAnimationFrame,
|
|
129
|
+
cancelAnimationFrame: clearTimeout,
|
|
130
|
+
addEventListener() {},
|
|
131
|
+
removeEventListener() {}
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
HTMLCanvasElement: HeadlessCanvas,
|
|
135
|
+
document: {
|
|
136
|
+
createElement: (tag) => tag === "canvas" ? new HeadlessCanvas() : {},
|
|
137
|
+
createElementNS: (_ns, tag) => tag === "canvas" ? new HeadlessCanvas() : {},
|
|
138
|
+
addEventListener() {},
|
|
139
|
+
removeEventListener() {}
|
|
140
|
+
},
|
|
141
|
+
window,
|
|
142
|
+
self: window,
|
|
143
|
+
requestAnimationFrame,
|
|
144
|
+
cancelAnimationFrame: clearTimeout
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Jest environment that puts real headless WebGPU (Dawn via the `webgpu` package) on the sandbox
|
|
149
|
+
* global: `navigator.gpu`, every `GPU*` class and constant, and a `HeadlessCanvas` shim. CommonJS
|
|
150
|
+
* tests can rely on `createCanvas` being a global; ESM tests can also `import` it from this package.
|
|
151
|
+
*/
|
|
152
|
+
var WebgpuEnvironment = class extends TestEnvironment {
|
|
153
|
+
#added = [];
|
|
154
|
+
#navigator = {};
|
|
155
|
+
#nativeAdded;
|
|
156
|
+
#dawnOptions;
|
|
157
|
+
constructor(config, context) {
|
|
158
|
+
super(config, context);
|
|
159
|
+
this.#nativeAdded = !Object.getOwnPropertyNames(this.global).includes("_native");
|
|
160
|
+
if (this.#nativeAdded) Object.defineProperty(this.global, "_native", {
|
|
161
|
+
value: void 0,
|
|
162
|
+
writable: true,
|
|
163
|
+
configurable: true,
|
|
164
|
+
enumerable: false
|
|
165
|
+
});
|
|
166
|
+
this.#dawnOptions = config.projectConfig.testEnvironmentOptions.dawnOptions;
|
|
167
|
+
}
|
|
168
|
+
async setup() {
|
|
169
|
+
await super.setup();
|
|
170
|
+
const { create, globals } = await import("webgpu");
|
|
171
|
+
const global = this.global;
|
|
172
|
+
const shims = {
|
|
173
|
+
...globals,
|
|
174
|
+
...domShims(),
|
|
175
|
+
createCanvas
|
|
176
|
+
};
|
|
177
|
+
this.#added = Object.keys(shims).filter((key) => !(key in global));
|
|
178
|
+
for (const key of this.#added) global[key] = shims[key];
|
|
179
|
+
if (this.#nativeAdded) this.#added.push("_native");
|
|
180
|
+
this.#navigator = global.navigator ??= {};
|
|
181
|
+
Object.defineProperty(this.#navigator, "gpu", {
|
|
182
|
+
value: create(this.#dawnOptions ?? []),
|
|
183
|
+
configurable: true
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
async teardown() {
|
|
187
|
+
delete this.#navigator.gpu;
|
|
188
|
+
const global = this.global;
|
|
189
|
+
for (const key of this.#added) delete global[key];
|
|
190
|
+
await super.teardown();
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
//#endregion
|
|
194
|
+
export { HeadlessCanvas, HeadlessCanvasContext, createCanvas, WebgpuEnvironment as default };
|
|
195
|
+
|
|
196
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/canvas.ts","../src/index.ts"],"sourcesContent":["/** RGBA8 pixels, top row first. Feed it to a screenshot matcher such as jest-image-snapshot after encoding to PNG. */\nexport type RgbaImage = { width: number; height: number; data: Uint8Array };\n\nconst COPY_SRC = 0x01;\nconst RENDER_ATTACHMENT = 0x10;\nconst MAP_READ = 0x01;\nconst COPY_DST = 0x08;\n\n/** Minimal `GPUCanvasContext`: `getCurrentTexture()` is a texture the size of the canvas, kept until reconfigured or resized. */\nexport class HeadlessCanvasContext {\n #config: GPUCanvasConfiguration | undefined;\n #texture: GPUTexture | undefined;\n\n constructor(readonly canvas: HeadlessCanvas) {}\n\n configure(config: GPUCanvasConfiguration): void {\n this.#config = config;\n this.#drop();\n }\n\n unconfigure(): void {\n this.#config = undefined;\n this.#drop();\n }\n\n getConfiguration(): GPUCanvasConfiguration | null {\n return this.#config ?? null;\n }\n\n getCurrentTexture(): GPUTexture {\n const config = this.#config;\n if (!config) throw new Error('getCurrentTexture() called before configure()');\n const { width, height } = this.canvas;\n if (this.#texture && (this.#texture.width !== width || this.#texture.height !== height)) this.#drop();\n this.#texture ??= config.device.createTexture({\n size: [width, height],\n format: config.format,\n usage: (config.usage ?? RENDER_ATTACHMENT) | RENDER_ATTACHMENT | COPY_SRC,\n viewFormats: config.viewFormats,\n });\n return this.#texture;\n }\n\n /** Copies the current texture to the CPU. BGRA formats are swizzled to RGBA. */\n async readPixels(): Promise<RgbaImage> {\n const texture = this.getCurrentTexture();\n const { device, format } = this.#config!;\n const { width, height } = texture;\n const bytesPerRow = Math.ceil((width * 4) / 256) * 256;\n const buffer = device.createBuffer({ size: bytesPerRow * height, usage: MAP_READ | COPY_DST });\n const encoder = device.createCommandEncoder();\n encoder.copyTextureToBuffer({ texture }, { buffer, bytesPerRow }, [width, height]);\n device.queue.submit([encoder.finish()]);\n await buffer.mapAsync(MAP_READ);\n const padded = new Uint8Array(buffer.getMappedRange());\n const data = new Uint8Array(width * height * 4);\n for (let y = 0; y < height; y++)\n data.set(padded.subarray(y * bytesPerRow, y * bytesPerRow + width * 4), y * width * 4);\n buffer.unmap();\n buffer.destroy();\n if (format.startsWith('bgra')) {\n for (let i = 0; i < data.length; i += 4) [data[i], data[i + 2]] = [data[i + 2]!, data[i]!];\n }\n return { width, height, data };\n }\n\n #drop(): void {\n this.#texture?.destroy();\n this.#texture = undefined;\n }\n}\n\n/** Enough of `HTMLCanvasElement` for three.js `WebGPURenderer`, Babylon Lite and friends. */\nexport class HeadlessCanvas {\n readonly #context = new HeadlessCanvasContext(this);\n readonly style: Record<string, string> = {};\n readonly #attributes = new Map<string, string>();\n\n constructor(\n public width = 300,\n public height = 150,\n ) {}\n\n get clientWidth(): number {\n return this.width;\n }\n\n get clientHeight(): number {\n return this.height;\n }\n\n getContext(id: 'webgpu'): HeadlessCanvasContext;\n getContext(id: string): HeadlessCanvasContext | null;\n getContext(id: string): HeadlessCanvasContext | null {\n return id === 'webgpu' ? this.#context : null;\n }\n\n readPixels(): Promise<RgbaImage> {\n return this.#context.readPixels();\n }\n\n /** This same object, typed as an element for libraries whose signatures demand one. */\n asElement(): HTMLCanvasElement {\n return this as unknown as HTMLCanvasElement;\n }\n\n getBoundingClientRect(): { x: number; y: number; width: number; height: number; top: number; left: number } {\n return { x: 0, y: 0, top: 0, left: 0, width: this.width, height: this.height };\n }\n\n setAttribute(name: string, value: string): void {\n this.#attributes.set(name, value);\n }\n\n getAttribute(name: string): string | null {\n return this.#attributes.get(name) ?? null;\n }\n\n addEventListener(): void {}\n removeEventListener(): void {}\n dispatchEvent(): boolean {\n return true;\n }\n}\n\nexport const createCanvas = (width: number, height: number): HeadlessCanvas => new HeadlessCanvas(width, height);\n","import { TestEnvironment } from 'jest-environment-node';\nimport { createCanvas, HeadlessCanvas } from './canvas.js';\n\ntype JestEnvironmentConfig = ConstructorParameters<typeof TestEnvironment>[0];\ntype EnvironmentContext = ConstructorParameters<typeof TestEnvironment>[1];\n\nexport { createCanvas, HeadlessCanvas, HeadlessCanvasContext, type RgbaImage } from './canvas.js';\n\n/** Set under `testEnvironmentOptions` in your Jest config. */\nexport type WebgpuNodeOptions = {\n /** Dawn options, e.g. `['backend=vulkan', 'enable-dawn-features=allow_unsafe_apis']`. */\n dawnOptions?: string[];\n};\n\n/** Just enough DOM for libraries that make their own canvas and drive a frame loop. Only installed when missing. */\nconst requestAnimationFrame = (callback: (time: number) => void): NodeJS.Timeout =>\n setTimeout(() => callback(performance.now()), 16).unref();\n\nconst domShims = (): Record<string, unknown> => {\n const window = {\n devicePixelRatio: 1,\n requestAnimationFrame,\n cancelAnimationFrame: clearTimeout,\n addEventListener() {},\n removeEventListener() {},\n };\n return {\n HTMLCanvasElement: HeadlessCanvas,\n document: {\n createElement: (tag: string) => (tag === 'canvas' ? new HeadlessCanvas() : {}),\n createElementNS: (_ns: string, tag: string) => (tag === 'canvas' ? new HeadlessCanvas() : {}),\n addEventListener() {},\n removeEventListener() {},\n },\n window,\n self: window,\n requestAnimationFrame,\n cancelAnimationFrame: clearTimeout,\n };\n};\n\n/**\n * Jest environment that puts real headless WebGPU (Dawn via the `webgpu` package) on the sandbox\n * global: `navigator.gpu`, every `GPU*` class and constant, and a `HeadlessCanvas` shim. CommonJS\n * tests can rely on `createCanvas` being a global; ESM tests can also `import` it from this package.\n */\nexport default class WebgpuEnvironment extends TestEnvironment {\n #added: string[] = [];\n #navigator: { gpu?: GPU } = {};\n #nativeAdded: boolean;\n #dawnOptions: string[] | undefined;\n\n constructor(config: JestEnvironmentConfig, context: EnvironmentContext) {\n super(config, context);\n // @babylonjs/core defines a `_native` accessor on `self` at module load; on Jest's proxied\n // sandbox global that defineProperty violates a proxy invariant, so pre-seed the property to\n // satisfy Babylon's hasOwnProperty guard.\n // `Object.hasOwn`/`in` report false negatives here: jest-environment-node's sandbox global is\n // a Proxy over a vm context, and reads of freshly-added properties from outside the sandbox\n // (as this constructor runs) don't go through the fast path those use. getOwnPropertyNames is\n // unaffected, so use that instead.\n this.#nativeAdded = !Object.getOwnPropertyNames(this.global).includes('_native');\n if (this.#nativeAdded) {\n Object.defineProperty(this.global, '_native', {\n value: undefined,\n writable: true,\n configurable: true,\n enumerable: false,\n });\n }\n this.#dawnOptions = config.projectConfig.testEnvironmentOptions.dawnOptions as string[] | undefined;\n }\n\n // `webgpu` is ESM-only. Loading it via a dynamic import here (rather than a static top-level\n // import) keeps it out of this module's own top-level module graph, so a CommonJS test file's\n // `require('jest-environment-webgpu-node')` - which only needs `createCanvas` - never forces\n // Jest's sandboxed CJS loader to load an ESM-only dependency (jestjs/jest#15716). This method\n // itself runs outside that sandbox, through Jest's own environment setup, where Node's native\n // `import()` always works.\n override async setup(): Promise<void> {\n await super.setup();\n const { create, globals } = await import('webgpu');\n const global = this.global as unknown as Record<string, unknown>;\n const shims = { ...globals, ...domShims(), createCanvas } as Record<string, unknown>;\n this.#added = Object.keys(shims).filter((key) => !(key in global));\n for (const key of this.#added) global[key] = shims[key];\n if (this.#nativeAdded) this.#added.push('_native');\n this.#navigator = (global.navigator ??= {}) as { gpu?: GPU };\n Object.defineProperty(this.#navigator, 'gpu', { value: create(this.#dawnOptions ?? []), configurable: true });\n }\n\n override async teardown(): Promise<void> {\n delete this.#navigator.gpu;\n const global = this.global as unknown as Record<string, unknown>;\n for (const key of this.#added) delete global[key];\n await super.teardown();\n }\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jest-environment-webgpu-node",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Jest environment that provides real headless WebGPU (Dawn via the webgpu package) in Node",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dawn",
|
|
7
|
+
"headless",
|
|
8
|
+
"jest",
|
|
9
|
+
"jest-environment",
|
|
10
|
+
"testing",
|
|
11
|
+
"webgpu"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/bhouston/jest-gpu#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/bhouston/jest-gpu/issues"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Ben Houston <ben@ben3d.ca> (https://ben3d.ca)",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/bhouston/jest-gpu.git",
|
|
22
|
+
"directory": "packages/jest-environment-webgpu-node"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist/**/*.cjs",
|
|
26
|
+
"dist/**/*.cjs.map",
|
|
27
|
+
"dist/**/*.mjs",
|
|
28
|
+
"dist/**/*.mjs.map",
|
|
29
|
+
"dist/**/*.d.cts",
|
|
30
|
+
"dist/**/*.d.cts.map",
|
|
31
|
+
"dist/**/*.d.mts",
|
|
32
|
+
"dist/**/*.d.mts.map",
|
|
33
|
+
"src/globals.d.ts",
|
|
34
|
+
"CHANGELOG.md"
|
|
35
|
+
],
|
|
36
|
+
"type": "module",
|
|
37
|
+
"main": "./dist/index.cjs",
|
|
38
|
+
"module": "./dist/index.mjs",
|
|
39
|
+
"types": "./dist/index.d.mts",
|
|
40
|
+
"exports": {
|
|
41
|
+
".": {
|
|
42
|
+
"import": {
|
|
43
|
+
"types": "./dist/index.d.mts",
|
|
44
|
+
"default": "./dist/index.mjs"
|
|
45
|
+
},
|
|
46
|
+
"require": {
|
|
47
|
+
"types": "./dist/index.d.cts",
|
|
48
|
+
"default": "./dist/index.cjs"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"./globals": {
|
|
52
|
+
"types": "./src/globals.d.ts"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsdown",
|
|
60
|
+
"tsc": "tsc --noEmit"
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"@webgpu/types": "^0.1.72",
|
|
64
|
+
"webgpu": "^0.6.1"
|
|
65
|
+
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"jest": ">=29 <31",
|
|
68
|
+
"jest-environment-node": ">=29 <31"
|
|
69
|
+
},
|
|
70
|
+
"engines": {
|
|
71
|
+
"node": ">=22.12.0"
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/globals.d.ts
ADDED