jest-environment-webgpu-node 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -8
- package/dist/index.cjs +53 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +81 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +53 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -9
- package/CHANGELOG.md +0 -5
package/README.md
CHANGED
|
@@ -1,11 +1,30 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://npmjs.com/package/jest-environment-webgpu-node"><img src="https://img.shields.io/npm/v/jest-environment-webgpu-node.svg" alt="npm package"></a>
|
|
3
|
+
<a href="https://www.npmtrends.com/jest-environment-webgpu-node"><img src="https://img.shields.io/npm/dw/jest-environment-webgpu-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-webgpu-node
|
|
2
11
|
|
|
3
|
-
Real
|
|
4
|
-
|
|
12
|
+
> Real WebGPU tests in plain Jest, with no browser.
|
|
13
|
+
|
|
14
|
+
- 🖥️ Real WebGPU via Dawn
|
|
15
|
+
- 🚫 No Browser or Mocks
|
|
16
|
+
- 🧩 Headless Canvas Included
|
|
17
|
+
- 🔧 Dawn Backend Options
|
|
18
|
+
- 📦 Dual ESM and CommonJS
|
|
19
|
+
- ⚡ Up to 2.4x Faster Than Browsers
|
|
20
|
+
|
|
21
|
+
Real, headless WebGPU inside Jest, powered by Google's Dawn through the
|
|
22
|
+
[`webgpu`](https://github.com/dawn-gpu/node-webgpu) npm package. Native GPU testing runs up to 2.4x faster than the
|
|
23
|
+
same tests in a browser, see [the write-up](https://ben3d.ca/blog/native-gpu-testing-for-vitest-and-jest).
|
|
24
|
+
|
|
25
|
+
Part of [jest-gpu](https://github.com/bhouston/jest-gpu), which also provides the WebGL environment.
|
|
5
26
|
|
|
6
|
-
|
|
7
|
-
`GPUShaderStage`, ...) on the test's global object before each test file and removes them
|
|
8
|
-
afterwards.
|
|
27
|
+
[Read the Docs to Learn More](https://jest-gpu.ben3d.ca/docs/webgpu/) for the full WebGPU guide, CommonJS and ESM setup, CI configuration and the API reference.
|
|
9
28
|
|
|
10
29
|
## Install
|
|
11
30
|
|
|
@@ -35,6 +54,20 @@ it('runs a compute shader', async () => {
|
|
|
35
54
|
`dawnOptions` are passed straight to Dawn: `backend=<null|d3d11|d3d12|metal|vulkan|opengl|opengles>`,
|
|
36
55
|
`adapter=<name>`, `enable-dawn-features=...`, `disable-dawn-features=...`.
|
|
37
56
|
|
|
57
|
+
## Linux and CI
|
|
58
|
+
|
|
59
|
+
Dawn needs Vulkan. On a Linux box or CI runner without a real GPU, install Mesa's software Vulkan
|
|
60
|
+
driver (lavapipe) and force software rendering:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
sudo apt-get update && sudo apt-get install -y libegl1 libgles2 libgl1-mesa-dri mesa-vulkan-drivers
|
|
64
|
+
export LIBGL_ALWAYS_SOFTWARE=1
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This repo's own [CI workflow](https://github.com/bhouston/jest-gpu/blob/main/.github/workflows/ci.yml) does exactly this, so WebGPU tests run
|
|
68
|
+
for real (not mocked) on `ubuntu-latest` on every PR. macOS runners use Dawn's Metal backend and need no
|
|
69
|
+
extra setup.
|
|
70
|
+
|
|
38
71
|
## Canvas
|
|
39
72
|
|
|
40
73
|
Dawn has no `<canvas>`, so the environment ships a headless one whose `getContext('webgpu')` returns a
|
|
@@ -73,14 +106,18 @@ test file itself imports an ESM-only library, such as `three/webgpu`.
|
|
|
73
106
|
|
|
74
107
|
## Using Vitest?
|
|
75
108
|
|
|
76
|
-
The equivalent package for Vitest is
|
|
109
|
+
The equivalent package for Vitest (see the [vitest-gpu docs](https://vitest-gpu.ben3d.ca/)) is
|
|
77
110
|
[`vitest-environment-webgpu-node`](https://github.com/bhouston/vitest-gpu/tree/main/packages/vitest-environment-webgpu-node)
|
|
78
111
|
from [vitest-gpu](https://github.com/bhouston/vitest-gpu).
|
|
79
112
|
|
|
80
|
-
##
|
|
113
|
+
## Contribution
|
|
81
114
|
|
|
82
|
-
|
|
115
|
+
See [Contributing Guide](https://github.com/bhouston/jest-gpu/blob/main/CONTRIBUTING.md).
|
|
83
116
|
|
|
84
117
|
## License
|
|
85
118
|
|
|
86
119
|
MIT
|
|
120
|
+
|
|
121
|
+
## Author
|
|
122
|
+
|
|
123
|
+
[Ben Houston](https://ben3d.ca), Sponsored by [Land of Assets](https://landofassets.com).
|
package/dist/index.cjs
CHANGED
|
@@ -17,20 +17,34 @@ var HeadlessCanvasContext = class {
|
|
|
17
17
|
canvas;
|
|
18
18
|
#config;
|
|
19
19
|
#texture;
|
|
20
|
+
/**
|
|
21
|
+
* Creates a context for a canvas; `HeadlessCanvas` does this for you.
|
|
22
|
+
* @param canvas - The canvas this context renders into; its current size sets the texture size.
|
|
23
|
+
*/
|
|
20
24
|
constructor(canvas) {
|
|
21
25
|
this.canvas = canvas;
|
|
22
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Stores the configuration and discards any current texture, as `GPUCanvasContext.configure()` does.
|
|
29
|
+
* @param config - The device, format and usage to render with.
|
|
30
|
+
*/
|
|
23
31
|
configure(config) {
|
|
24
32
|
this.#config = config;
|
|
25
33
|
this.#drop();
|
|
26
34
|
}
|
|
35
|
+
/** Clears the configuration and destroys the current texture. */
|
|
27
36
|
unconfigure() {
|
|
28
37
|
this.#config = void 0;
|
|
29
38
|
this.#drop();
|
|
30
39
|
}
|
|
40
|
+
/** Returns the configuration passed to `configure()`, or `null` before it is called or after `unconfigure()`. */
|
|
31
41
|
getConfiguration() {
|
|
32
42
|
return this.#config ?? null;
|
|
33
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Returns the texture to render into, creating it at the canvas's current size on first use or after a resize.
|
|
46
|
+
* @throws If `configure()` has not been called.
|
|
47
|
+
*/
|
|
34
48
|
getCurrentTexture() {
|
|
35
49
|
const config = this.#config;
|
|
36
50
|
if (!config) throw new Error("getCurrentTexture() called before configure()");
|
|
@@ -44,7 +58,10 @@ var HeadlessCanvasContext = class {
|
|
|
44
58
|
});
|
|
45
59
|
return this.#texture;
|
|
46
60
|
}
|
|
47
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Copies the current texture to the CPU. BGRA formats are swizzled to RGBA.
|
|
63
|
+
* @throws If the context is not configured or the format is not 8-bit RGBA or BGRA.
|
|
64
|
+
*/
|
|
48
65
|
async readPixels() {
|
|
49
66
|
const config = this.#config;
|
|
50
67
|
if (!config) throw new Error("readPixels() called before configure()");
|
|
@@ -89,21 +106,30 @@ var HeadlessCanvas = class {
|
|
|
89
106
|
width;
|
|
90
107
|
height;
|
|
91
108
|
#context = new HeadlessCanvasContext(this);
|
|
109
|
+
/** Style properties libraries may set; stored but not applied to anything. */
|
|
92
110
|
style = {};
|
|
93
111
|
#attributes = /* @__PURE__ */ new Map();
|
|
112
|
+
/**
|
|
113
|
+
* Creates a canvas of the given size; see also `createCanvas()`.
|
|
114
|
+
* @param width - Canvas width in pixels.
|
|
115
|
+
* @param height - Canvas height in pixels.
|
|
116
|
+
*/
|
|
94
117
|
constructor(width = 300, height = 150) {
|
|
95
118
|
this.width = width;
|
|
96
119
|
this.height = height;
|
|
97
120
|
}
|
|
121
|
+
/** Same as `width`. */
|
|
98
122
|
get clientWidth() {
|
|
99
123
|
return this.width;
|
|
100
124
|
}
|
|
125
|
+
/** Same as `height`. */
|
|
101
126
|
get clientHeight() {
|
|
102
127
|
return this.height;
|
|
103
128
|
}
|
|
104
129
|
getContext(id) {
|
|
105
130
|
return id === "webgpu" ? this.#context : null;
|
|
106
131
|
}
|
|
132
|
+
/** Reads back the canvas's current texture as RGBA8 pixels; see `HeadlessCanvasContext.readPixels()`. */
|
|
107
133
|
readPixels() {
|
|
108
134
|
return this.#context.readPixels();
|
|
109
135
|
}
|
|
@@ -111,6 +137,7 @@ var HeadlessCanvas = class {
|
|
|
111
137
|
asElement() {
|
|
112
138
|
return this;
|
|
113
139
|
}
|
|
140
|
+
/** Returns a rectangle at the origin with the canvas's size. */
|
|
114
141
|
getBoundingClientRect() {
|
|
115
142
|
return {
|
|
116
143
|
x: 0,
|
|
@@ -121,18 +148,36 @@ var HeadlessCanvas = class {
|
|
|
121
148
|
height: this.height
|
|
122
149
|
};
|
|
123
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Stores an attribute for `getAttribute()`.
|
|
153
|
+
* @param name - Attribute name.
|
|
154
|
+
* @param value - Attribute value.
|
|
155
|
+
*/
|
|
124
156
|
setAttribute(name, value) {
|
|
125
157
|
this.#attributes.set(name, value);
|
|
126
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Returns an attribute stored with `setAttribute()`, or `null` when unset.
|
|
161
|
+
* @param name - Attribute name.
|
|
162
|
+
*/
|
|
127
163
|
getAttribute(name) {
|
|
128
164
|
return this.#attributes.get(name) ?? null;
|
|
129
165
|
}
|
|
166
|
+
/** Does nothing; the canvas dispatches no events. */
|
|
130
167
|
addEventListener() {}
|
|
168
|
+
/** Does nothing. */
|
|
131
169
|
removeEventListener() {}
|
|
170
|
+
/** Does nothing and returns `true`. */
|
|
132
171
|
dispatchEvent() {
|
|
133
172
|
return true;
|
|
134
173
|
}
|
|
135
174
|
};
|
|
175
|
+
/**
|
|
176
|
+
* Creates a headless canvas whose `getContext('webgpu')` returns a texture-backed `GPUCanvasContext`. Also installed
|
|
177
|
+
* as a global in the test environment.
|
|
178
|
+
* @param width - Canvas width in pixels.
|
|
179
|
+
* @param height - Canvas height in pixels.
|
|
180
|
+
*/
|
|
136
181
|
const createCanvas = (width, height) => new HeadlessCanvas(width, height);
|
|
137
182
|
//#endregion
|
|
138
183
|
//#region src/index.ts
|
|
@@ -179,6 +224,11 @@ var WebgpuEnvironment = class extends jest_environment_node.TestEnvironment {
|
|
|
179
224
|
#nativeAdded;
|
|
180
225
|
#dawnOptions;
|
|
181
226
|
#animationFrames = /* @__PURE__ */ new Map();
|
|
227
|
+
/**
|
|
228
|
+
* Creates the environment; Jest calls this once per test file.
|
|
229
|
+
* @param config - Jest's environment config; `testEnvironmentOptions.dawnOptions` is read from it.
|
|
230
|
+
* @param context - Jest's environment context.
|
|
231
|
+
*/
|
|
182
232
|
constructor(config, context) {
|
|
183
233
|
const { dawnOptions, ...testEnvironmentOptions } = config.projectConfig.testEnvironmentOptions;
|
|
184
234
|
super({
|
|
@@ -197,6 +247,7 @@ var WebgpuEnvironment = class extends jest_environment_node.TestEnvironment {
|
|
|
197
247
|
enumerable: false
|
|
198
248
|
});
|
|
199
249
|
}
|
|
250
|
+
/** Loads Dawn, then installs `navigator.gpu`, the `GPU*` globals, the DOM shims and `createCanvas` on the sandbox. */
|
|
200
251
|
async setup() {
|
|
201
252
|
await super.setup();
|
|
202
253
|
const { create, globals } = await import("webgpu");
|
|
@@ -222,6 +273,7 @@ var WebgpuEnvironment = class extends jest_environment_node.TestEnvironment {
|
|
|
222
273
|
writable: true
|
|
223
274
|
});
|
|
224
275
|
}
|
|
276
|
+
/** Cancels pending animation frames and removes everything `setup()` installed from the sandbox. */
|
|
225
277
|
async teardown() {
|
|
226
278
|
const global = this.global;
|
|
227
279
|
for (const [handle, clear] of this.#animationFrames) clear(handle);
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +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;\nconst READABLE_FORMATS = new Set<GPUTextureFormat>(['rgba8unorm', 'rgba8unorm-srgb', 'bgra8unorm', 'bgra8unorm-srgb']);\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 config = this.#config;\n if (!config) throw new Error('readPixels() called before configure()');\n const { device, format } = config;\n if (!READABLE_FORMATS.has(format)) {\n throw new Error(`readPixels() supports only 8-bit RGBA/BGRA formats; received ${format}`);\n }\n const texture = this.getCurrentTexture();\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 try {\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 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 } finally {\n if (buffer.mapState === 'mapped') buffer.unmap();\n buffer.destroy();\n }\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. */\ntype TimerHandle = ReturnType<typeof setTimeout>;\ntype SandboxGlobal = Record<string, unknown> & {\n clearTimeout: typeof clearTimeout;\n performance: Performance;\n setTimeout: typeof setTimeout;\n};\n\nconst domShims = (\n global: SandboxGlobal,\n animationFrames: Map<TimerHandle, typeof clearTimeout>,\n): Record<string, unknown> => {\n const requestAnimationFrame = (callback: (time: number) => void): TimerHandle => {\n const handle = global.setTimeout(() => {\n animationFrames.delete(handle);\n callback(global.performance.now());\n }, 16);\n animationFrames.set(handle, global.clearTimeout);\n (handle as NodeJS.Timeout).unref?.();\n return handle;\n };\n const cancelAnimationFrame = (handle: TimerHandle): void => {\n const clear = animationFrames.get(handle) ?? global.clearTimeout;\n animationFrames.delete(handle);\n clear(handle);\n };\n const 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 return {\n document,\n devicePixelRatio: 1,\n requestAnimationFrame,\n cancelAnimationFrame,\n addEventListener() {},\n removeEventListener() {},\n HTMLCanvasElement: HeadlessCanvas,\n window: global,\n self: global,\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 #hostNavigator: PropertyDescriptor | undefined;\n #nativeAdded: boolean;\n #dawnOptions: string[] | undefined;\n #animationFrames = new Map<TimerHandle, typeof clearTimeout>();\n\n constructor(config: JestEnvironmentConfig, context: EnvironmentContext) {\n // jest-environment-node assigns every `testEnvironmentOptions` entry onto the sandbox global, so\n // strip this environment's own option first to keep `dawnOptions` off `globalThis`.\n const { dawnOptions, ...testEnvironmentOptions } = config.projectConfig.testEnvironmentOptions;\n super({ ...config, projectConfig: { ...config.projectConfig, testEnvironmentOptions } }, context);\n this.#dawnOptions = dawnOptions as string[] | undefined;\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 }\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 SandboxGlobal;\n const shims = { ...globals, ...domShims(global, this.#animationFrames), 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 // jest-environment-node hands the sandbox Node's own `navigator` by reference, so adding `gpu` to it\n // would leak onto the host process and be shared by every environment in this worker. Give the\n // sandbox its own proxy instead; `Reflect.get(target, key, target)` keeps Node's private-field\n // getters (hardwareConcurrency, userAgent, ...) working through it.\n const gpu = create(this.#dawnOptions ?? []);\n const host = (global.navigator ?? {}) as object;\n this.#hostNavigator = Object.getOwnPropertyDescriptor(global, 'navigator');\n Object.defineProperty(global, 'navigator', {\n value: new Proxy(host, {\n get: (target, key) => (key === 'gpu' ? gpu : Reflect.get(target, key, target)),\n has: (target, key) => key === 'gpu' || Reflect.has(target, key),\n }),\n configurable: true,\n enumerable: true,\n writable: true,\n });\n }\n\n override async teardown(): Promise<void> {\n const global = this.global as unknown as Record<string, unknown>;\n for (const [handle, clear] of this.#animationFrames) clear(handle);\n this.#animationFrames.clear();\n if (this.#hostNavigator) Object.defineProperty(global, 'navigator', this.#hostNavigator);\n else delete global.navigator;\n for (const key of this.#added) delete global[key];\n await super.teardown();\n }\n}\n"],"mappings":"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"}
|
|
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 = {\n /** Width in pixels. */\n width: number;\n /** Height in pixels. */\n height: number;\n /** Pixel bytes, four per pixel in R, G, B, A order, rows top to bottom with no padding. */\n data: Uint8Array;\n};\n\nconst COPY_SRC = 0x01;\nconst RENDER_ATTACHMENT = 0x10;\nconst MAP_READ = 0x01;\nconst COPY_DST = 0x08;\nconst READABLE_FORMATS = new Set<GPUTextureFormat>(['rgba8unorm', 'rgba8unorm-srgb', 'bgra8unorm', 'bgra8unorm-srgb']);\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 /**\n * Creates a context for a canvas; `HeadlessCanvas` does this for you.\n * @param canvas - The canvas this context renders into; its current size sets the texture size.\n */\n constructor(\n /** The canvas that owns this context. */\n readonly canvas: HeadlessCanvas,\n ) {}\n\n /**\n * Stores the configuration and discards any current texture, as `GPUCanvasContext.configure()` does.\n * @param config - The device, format and usage to render with.\n */\n configure(config: GPUCanvasConfiguration): void {\n this.#config = config;\n this.#drop();\n }\n\n /** Clears the configuration and destroys the current texture. */\n unconfigure(): void {\n this.#config = undefined;\n this.#drop();\n }\n\n /** Returns the configuration passed to `configure()`, or `null` before it is called or after `unconfigure()`. */\n getConfiguration(): GPUCanvasConfiguration | null {\n return this.#config ?? null;\n }\n\n /**\n * Returns the texture to render into, creating it at the canvas's current size on first use or after a resize.\n * @throws If `configure()` has not been called.\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 /**\n * Copies the current texture to the CPU. BGRA formats are swizzled to RGBA.\n * @throws If the context is not configured or the format is not 8-bit RGBA or BGRA.\n */\n async readPixels(): Promise<RgbaImage> {\n const config = this.#config;\n if (!config) throw new Error('readPixels() called before configure()');\n const { device, format } = config;\n if (!READABLE_FORMATS.has(format)) {\n throw new Error(`readPixels() supports only 8-bit RGBA/BGRA formats; received ${format}`);\n }\n const texture = this.getCurrentTexture();\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 try {\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 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 } finally {\n if (buffer.mapState === 'mapped') buffer.unmap();\n buffer.destroy();\n }\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 /** Style properties libraries may set; stored but not applied to anything. */\n readonly style: Record<string, string> = {};\n readonly #attributes = new Map<string, string>();\n\n /**\n * Creates a canvas of the given size; see also `createCanvas()`.\n * @param width - Canvas width in pixels.\n * @param height - Canvas height in pixels.\n */\n constructor(\n /** Canvas width in pixels. Change it to resize the render target. */\n public width = 300,\n /** Canvas height in pixels. Change it to resize the render target. */\n public height = 150,\n ) {}\n\n /** Same as `width`. */\n get clientWidth(): number {\n return this.width;\n }\n\n /** Same as `height`. */\n get clientHeight(): number {\n return this.height;\n }\n\n /**\n * Returns this canvas's WebGPU context.\n * @param id - The context type, `'webgpu'`.\n */\n getContext(id: 'webgpu'): HeadlessCanvasContext;\n /**\n * Returns this canvas's WebGPU context for `'webgpu'`, or `null` for any other type.\n * @param id - The context type requested.\n */\n getContext(id: string): HeadlessCanvasContext | null;\n getContext(id: string): HeadlessCanvasContext | null {\n return id === 'webgpu' ? this.#context : null;\n }\n\n /** Reads back the canvas's current texture as RGBA8 pixels; see `HeadlessCanvasContext.readPixels()`. */\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 /** Returns a rectangle at the origin with the canvas's size. */\n getBoundingClientRect(): {\n /** Always 0. */\n x: number;\n /** Always 0. */\n y: number;\n /** The canvas width. */\n width: number;\n /** The canvas height. */\n height: number;\n /** Always 0. */\n top: number;\n /** Always 0. */\n left: number;\n } {\n return { x: 0, y: 0, top: 0, left: 0, width: this.width, height: this.height };\n }\n\n /**\n * Stores an attribute for `getAttribute()`.\n * @param name - Attribute name.\n * @param value - Attribute value.\n */\n setAttribute(name: string, value: string): void {\n this.#attributes.set(name, value);\n }\n\n /**\n * Returns an attribute stored with `setAttribute()`, or `null` when unset.\n * @param name - Attribute name.\n */\n getAttribute(name: string): string | null {\n return this.#attributes.get(name) ?? null;\n }\n\n /** Does nothing; the canvas dispatches no events. */\n addEventListener(): void {}\n /** Does nothing. */\n removeEventListener(): void {}\n /** Does nothing and returns `true`. */\n dispatchEvent(): boolean {\n return true;\n }\n}\n\n/**\n * Creates a headless canvas whose `getContext('webgpu')` returns a texture-backed `GPUCanvasContext`. Also installed\n * as a global in the test environment.\n * @param width - Canvas width in pixels.\n * @param height - Canvas height in pixels.\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. */\ntype TimerHandle = ReturnType<typeof setTimeout>;\ntype SandboxGlobal = Record<string, unknown> & {\n clearTimeout: typeof clearTimeout;\n performance: Performance;\n setTimeout: typeof setTimeout;\n};\n\nconst domShims = (\n global: SandboxGlobal,\n animationFrames: Map<TimerHandle, typeof clearTimeout>,\n): Record<string, unknown> => {\n const requestAnimationFrame = (callback: (time: number) => void): TimerHandle => {\n const handle = global.setTimeout(() => {\n animationFrames.delete(handle);\n callback(global.performance.now());\n }, 16);\n animationFrames.set(handle, global.clearTimeout);\n handle.unref?.();\n return handle;\n };\n const cancelAnimationFrame = (handle: TimerHandle): void => {\n const clear = animationFrames.get(handle) ?? global.clearTimeout;\n animationFrames.delete(handle);\n clear(handle);\n };\n const 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 return {\n document,\n devicePixelRatio: 1,\n requestAnimationFrame,\n cancelAnimationFrame,\n addEventListener() {},\n removeEventListener() {},\n HTMLCanvasElement: HeadlessCanvas,\n window: global,\n self: global,\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 #hostNavigator: PropertyDescriptor | undefined;\n #nativeAdded: boolean;\n #dawnOptions: string[] | undefined;\n #animationFrames = new Map<TimerHandle, typeof clearTimeout>();\n\n /**\n * Creates the environment; Jest calls this once per test file.\n * @param config - Jest's environment config; `testEnvironmentOptions.dawnOptions` is read from it.\n * @param context - Jest's environment context.\n */\n constructor(config: JestEnvironmentConfig, context: EnvironmentContext) {\n // jest-environment-node assigns every `testEnvironmentOptions` entry onto the sandbox global, so\n // strip this environment's own option first to keep `dawnOptions` off `globalThis`.\n const { dawnOptions, ...testEnvironmentOptions } = config.projectConfig.testEnvironmentOptions;\n super({ ...config, projectConfig: { ...config.projectConfig, testEnvironmentOptions } }, context);\n this.#dawnOptions = dawnOptions as string[] | undefined;\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 }\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 /** Loads Dawn, then installs `navigator.gpu`, the `GPU*` globals, the DOM shims and `createCanvas` on the sandbox. */\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 SandboxGlobal;\n // oxlint-disable-next-line typescript/no-misused-spread -- intentionally copies the own enumerable GPU* globals\n const shims = { ...globals, ...domShims(global, this.#animationFrames), 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 // jest-environment-node hands the sandbox Node's own `navigator` by reference, so adding `gpu` to it\n // would leak onto the host process and be shared by every environment in this worker. Give the\n // sandbox its own proxy instead; `Reflect.get(target, key, target)` keeps Node's private-field\n // getters (hardwareConcurrency, userAgent, ...) working through it.\n const gpu = create(this.#dawnOptions ?? []);\n const host = global.navigator ?? {};\n this.#hostNavigator = Object.getOwnPropertyDescriptor(global, 'navigator');\n Object.defineProperty(global, 'navigator', {\n value: new Proxy(host, {\n get: (target, key) => (key === 'gpu' ? gpu : Reflect.get(target, key, target)),\n has: (target, key) => key === 'gpu' || Reflect.has(target, key),\n }),\n configurable: true,\n enumerable: true,\n writable: true,\n });\n }\n\n /** Cancels pending animation frames and removes everything `setup()` installed from the sandbox. */\n override async teardown(): Promise<void> {\n const global = this.global as unknown as Record<string, unknown>;\n for (const [handle, clear] of this.#animationFrames) clear(handle);\n this.#animationFrames.clear();\n if (this.#hostNavigator) Object.defineProperty(global, 'navigator', this.#hostNavigator);\n else delete global.navigator;\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
CHANGED
|
@@ -2,50 +2,121 @@ import { TestEnvironment } from "jest-environment-node";
|
|
|
2
2
|
//#region src/canvas.d.ts
|
|
3
3
|
/** RGBA8 pixels, top row first. Feed it to a screenshot matcher such as jest-image-snapshot after encoding to PNG. */
|
|
4
4
|
type RgbaImage = {
|
|
5
|
+
/** Width in pixels. */
|
|
5
6
|
width: number;
|
|
7
|
+
/** Height in pixels. */
|
|
6
8
|
height: number;
|
|
9
|
+
/** Pixel bytes, four per pixel in R, G, B, A order, rows top to bottom with no padding. */
|
|
7
10
|
data: Uint8Array;
|
|
8
11
|
};
|
|
9
12
|
/** Minimal `GPUCanvasContext`: `getCurrentTexture()` is a texture the size of the canvas, kept until reconfigured or resized. */
|
|
10
13
|
export declare class HeadlessCanvasContext {
|
|
11
14
|
#private;
|
|
15
|
+
/** The canvas that owns this context. */
|
|
12
16
|
readonly canvas: HeadlessCanvas;
|
|
13
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Creates a context for a canvas; `HeadlessCanvas` does this for you.
|
|
19
|
+
* @param canvas - The canvas this context renders into; its current size sets the texture size.
|
|
20
|
+
*/
|
|
21
|
+
constructor(
|
|
22
|
+
/** The canvas that owns this context. */
|
|
23
|
+
canvas: HeadlessCanvas);
|
|
24
|
+
/**
|
|
25
|
+
* Stores the configuration and discards any current texture, as `GPUCanvasContext.configure()` does.
|
|
26
|
+
* @param config - The device, format and usage to render with.
|
|
27
|
+
*/
|
|
14
28
|
configure(config: GPUCanvasConfiguration): void;
|
|
29
|
+
/** Clears the configuration and destroys the current texture. */
|
|
15
30
|
unconfigure(): void;
|
|
31
|
+
/** Returns the configuration passed to `configure()`, or `null` before it is called or after `unconfigure()`. */
|
|
16
32
|
getConfiguration(): GPUCanvasConfiguration | null;
|
|
33
|
+
/**
|
|
34
|
+
* Returns the texture to render into, creating it at the canvas's current size on first use or after a resize.
|
|
35
|
+
* @throws If `configure()` has not been called.
|
|
36
|
+
*/
|
|
17
37
|
getCurrentTexture(): GPUTexture;
|
|
18
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Copies the current texture to the CPU. BGRA formats are swizzled to RGBA.
|
|
40
|
+
* @throws If the context is not configured or the format is not 8-bit RGBA or BGRA.
|
|
41
|
+
*/
|
|
19
42
|
readPixels(): Promise<RgbaImage>;
|
|
20
43
|
}
|
|
21
44
|
/** Enough of `HTMLCanvasElement` for three.js `WebGPURenderer`, Babylon Lite and friends. */
|
|
22
45
|
export declare class HeadlessCanvas {
|
|
23
46
|
#private;
|
|
47
|
+
/** Canvas width in pixels. Change it to resize the render target. */
|
|
24
48
|
width: number;
|
|
49
|
+
/** Canvas height in pixels. Change it to resize the render target. */
|
|
25
50
|
height: number;
|
|
51
|
+
/** Style properties libraries may set; stored but not applied to anything. */
|
|
26
52
|
readonly style: Record<string, string>;
|
|
27
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Creates a canvas of the given size; see also `createCanvas()`.
|
|
55
|
+
* @param width - Canvas width in pixels.
|
|
56
|
+
* @param height - Canvas height in pixels.
|
|
57
|
+
*/
|
|
58
|
+
constructor(
|
|
59
|
+
/** Canvas width in pixels. Change it to resize the render target. */
|
|
60
|
+
width?: number,
|
|
61
|
+
/** Canvas height in pixels. Change it to resize the render target. */
|
|
62
|
+
height?: number);
|
|
63
|
+
/** Same as `width`. */
|
|
28
64
|
get clientWidth(): number;
|
|
65
|
+
/** Same as `height`. */
|
|
29
66
|
get clientHeight(): number;
|
|
67
|
+
/**
|
|
68
|
+
* Returns this canvas's WebGPU context.
|
|
69
|
+
* @param id - The context type, `'webgpu'`.
|
|
70
|
+
*/
|
|
30
71
|
getContext(id: 'webgpu'): HeadlessCanvasContext;
|
|
72
|
+
/**
|
|
73
|
+
* Returns this canvas's WebGPU context for `'webgpu'`, or `null` for any other type.
|
|
74
|
+
* @param id - The context type requested.
|
|
75
|
+
*/
|
|
31
76
|
getContext(id: string): HeadlessCanvasContext | null;
|
|
77
|
+
/** Reads back the canvas's current texture as RGBA8 pixels; see `HeadlessCanvasContext.readPixels()`. */
|
|
32
78
|
readPixels(): Promise<RgbaImage>;
|
|
33
79
|
/** This same object, typed as an element for libraries whose signatures demand one. */
|
|
34
80
|
asElement(): HTMLCanvasElement;
|
|
81
|
+
/** Returns a rectangle at the origin with the canvas's size. */
|
|
35
82
|
getBoundingClientRect(): {
|
|
83
|
+
/** Always 0. */
|
|
36
84
|
x: number;
|
|
85
|
+
/** Always 0. */
|
|
37
86
|
y: number;
|
|
87
|
+
/** The canvas width. */
|
|
38
88
|
width: number;
|
|
89
|
+
/** The canvas height. */
|
|
39
90
|
height: number;
|
|
91
|
+
/** Always 0. */
|
|
40
92
|
top: number;
|
|
93
|
+
/** Always 0. */
|
|
41
94
|
left: number;
|
|
42
95
|
};
|
|
96
|
+
/**
|
|
97
|
+
* Stores an attribute for `getAttribute()`.
|
|
98
|
+
* @param name - Attribute name.
|
|
99
|
+
* @param value - Attribute value.
|
|
100
|
+
*/
|
|
43
101
|
setAttribute(name: string, value: string): void;
|
|
102
|
+
/**
|
|
103
|
+
* Returns an attribute stored with `setAttribute()`, or `null` when unset.
|
|
104
|
+
* @param name - Attribute name.
|
|
105
|
+
*/
|
|
44
106
|
getAttribute(name: string): string | null;
|
|
107
|
+
/** Does nothing; the canvas dispatches no events. */
|
|
45
108
|
addEventListener(): void;
|
|
109
|
+
/** Does nothing. */
|
|
46
110
|
removeEventListener(): void;
|
|
111
|
+
/** Does nothing and returns `true`. */
|
|
47
112
|
dispatchEvent(): boolean;
|
|
48
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Creates a headless canvas whose `getContext('webgpu')` returns a texture-backed `GPUCanvasContext`. Also installed
|
|
116
|
+
* as a global in the test environment.
|
|
117
|
+
* @param width - Canvas width in pixels.
|
|
118
|
+
* @param height - Canvas height in pixels.
|
|
119
|
+
*/
|
|
49
120
|
export declare const createCanvas: (width: number, height: number) => HeadlessCanvas;
|
|
50
121
|
//#endregion
|
|
51
122
|
//#region src/index.d.ts
|
|
@@ -63,8 +134,15 @@ export type WebgpuNodeOptions = {
|
|
|
63
134
|
*/
|
|
64
135
|
export default class WebgpuEnvironment extends TestEnvironment {
|
|
65
136
|
#private;
|
|
137
|
+
/**
|
|
138
|
+
* Creates the environment; Jest calls this once per test file.
|
|
139
|
+
* @param config - Jest's environment config; `testEnvironmentOptions.dawnOptions` is read from it.
|
|
140
|
+
* @param context - Jest's environment context.
|
|
141
|
+
*/
|
|
66
142
|
constructor(config: JestEnvironmentConfig, context: EnvironmentContext);
|
|
143
|
+
/** Loads Dawn, then installs `navigator.gpu`, the `GPU*` globals, the DOM shims and `createCanvas` on the sandbox. */
|
|
67
144
|
setup(): Promise<void>;
|
|
145
|
+
/** Cancels pending animation frames and removes everything `setup()` installed from the sandbox. */
|
|
68
146
|
teardown(): Promise<void>;
|
|
69
147
|
}
|
|
70
148
|
//#endregion
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/canvas.ts","../src/index.ts"],"mappings":";;;KACY
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/canvas.ts","../src/index.ts"],"mappings":";;;KACY;;EAEV;;EAEA;;EAEA,MAAM;;;qBAUK;;;WAUA,QAAQ;;;;;EAFnB,YAEW;;EAAA,QAAQ;;;;;EAOnB,UAAU,QAAQ;;EAMlB;;EAMA,oBAAoB;;;;;EAQpB,qBAAqB;;;;;EAkBf,cAAc,QAAQ;;;qBAqCjB;;;EAaF;;EAEA;;WAZA,OAAO;;;;;;EAQhB,YAES;;EAAA,gBAEA;;EAAA;;MAIL;;MAKA;;;;;EAQJ,WAAW,eAAe;;;;;EAK1B,WAAW,aAAa;;EAMxB,cAAc,QAAQ;;EAKtB,aAAa;;EAKb;;IAEE;;IAEA;;IAEA;;IAEA;;IAEA;;IAEA;;;;;;;EAUF,aAAa,cAAc;;;;;EAQ3B,aAAa;;EAKb;;EAEA;;EAEA;;;;;;;;qBAWW,eAAY,eAAiB,mBAAmB;;;KCjNxD,wBAAwB,6BAA6B;KACrD,qBAAqB,6BAA6B;;YAK3C;;EAEV;;;;;;;qBAqDmB,0BAA0B;;;;;;;EAY7C,YAAY,QAAQ,uBAAuB,SAAS;;EA+BrC,SAAS;;EA4BT,YAAY"}
|
package/dist/index.d.mts
CHANGED
|
@@ -2,50 +2,121 @@ import { TestEnvironment } from "jest-environment-node";
|
|
|
2
2
|
//#region src/canvas.d.ts
|
|
3
3
|
/** RGBA8 pixels, top row first. Feed it to a screenshot matcher such as jest-image-snapshot after encoding to PNG. */
|
|
4
4
|
type RgbaImage = {
|
|
5
|
+
/** Width in pixels. */
|
|
5
6
|
width: number;
|
|
7
|
+
/** Height in pixels. */
|
|
6
8
|
height: number;
|
|
9
|
+
/** Pixel bytes, four per pixel in R, G, B, A order, rows top to bottom with no padding. */
|
|
7
10
|
data: Uint8Array;
|
|
8
11
|
};
|
|
9
12
|
/** Minimal `GPUCanvasContext`: `getCurrentTexture()` is a texture the size of the canvas, kept until reconfigured or resized. */
|
|
10
13
|
export declare class HeadlessCanvasContext {
|
|
11
14
|
#private;
|
|
15
|
+
/** The canvas that owns this context. */
|
|
12
16
|
readonly canvas: HeadlessCanvas;
|
|
13
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Creates a context for a canvas; `HeadlessCanvas` does this for you.
|
|
19
|
+
* @param canvas - The canvas this context renders into; its current size sets the texture size.
|
|
20
|
+
*/
|
|
21
|
+
constructor(
|
|
22
|
+
/** The canvas that owns this context. */
|
|
23
|
+
canvas: HeadlessCanvas);
|
|
24
|
+
/**
|
|
25
|
+
* Stores the configuration and discards any current texture, as `GPUCanvasContext.configure()` does.
|
|
26
|
+
* @param config - The device, format and usage to render with.
|
|
27
|
+
*/
|
|
14
28
|
configure(config: GPUCanvasConfiguration): void;
|
|
29
|
+
/** Clears the configuration and destroys the current texture. */
|
|
15
30
|
unconfigure(): void;
|
|
31
|
+
/** Returns the configuration passed to `configure()`, or `null` before it is called or after `unconfigure()`. */
|
|
16
32
|
getConfiguration(): GPUCanvasConfiguration | null;
|
|
33
|
+
/**
|
|
34
|
+
* Returns the texture to render into, creating it at the canvas's current size on first use or after a resize.
|
|
35
|
+
* @throws If `configure()` has not been called.
|
|
36
|
+
*/
|
|
17
37
|
getCurrentTexture(): GPUTexture;
|
|
18
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Copies the current texture to the CPU. BGRA formats are swizzled to RGBA.
|
|
40
|
+
* @throws If the context is not configured or the format is not 8-bit RGBA or BGRA.
|
|
41
|
+
*/
|
|
19
42
|
readPixels(): Promise<RgbaImage>;
|
|
20
43
|
}
|
|
21
44
|
/** Enough of `HTMLCanvasElement` for three.js `WebGPURenderer`, Babylon Lite and friends. */
|
|
22
45
|
export declare class HeadlessCanvas {
|
|
23
46
|
#private;
|
|
47
|
+
/** Canvas width in pixels. Change it to resize the render target. */
|
|
24
48
|
width: number;
|
|
49
|
+
/** Canvas height in pixels. Change it to resize the render target. */
|
|
25
50
|
height: number;
|
|
51
|
+
/** Style properties libraries may set; stored but not applied to anything. */
|
|
26
52
|
readonly style: Record<string, string>;
|
|
27
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Creates a canvas of the given size; see also `createCanvas()`.
|
|
55
|
+
* @param width - Canvas width in pixels.
|
|
56
|
+
* @param height - Canvas height in pixels.
|
|
57
|
+
*/
|
|
58
|
+
constructor(
|
|
59
|
+
/** Canvas width in pixels. Change it to resize the render target. */
|
|
60
|
+
width?: number,
|
|
61
|
+
/** Canvas height in pixels. Change it to resize the render target. */
|
|
62
|
+
height?: number);
|
|
63
|
+
/** Same as `width`. */
|
|
28
64
|
get clientWidth(): number;
|
|
65
|
+
/** Same as `height`. */
|
|
29
66
|
get clientHeight(): number;
|
|
67
|
+
/**
|
|
68
|
+
* Returns this canvas's WebGPU context.
|
|
69
|
+
* @param id - The context type, `'webgpu'`.
|
|
70
|
+
*/
|
|
30
71
|
getContext(id: 'webgpu'): HeadlessCanvasContext;
|
|
72
|
+
/**
|
|
73
|
+
* Returns this canvas's WebGPU context for `'webgpu'`, or `null` for any other type.
|
|
74
|
+
* @param id - The context type requested.
|
|
75
|
+
*/
|
|
31
76
|
getContext(id: string): HeadlessCanvasContext | null;
|
|
77
|
+
/** Reads back the canvas's current texture as RGBA8 pixels; see `HeadlessCanvasContext.readPixels()`. */
|
|
32
78
|
readPixels(): Promise<RgbaImage>;
|
|
33
79
|
/** This same object, typed as an element for libraries whose signatures demand one. */
|
|
34
80
|
asElement(): HTMLCanvasElement;
|
|
81
|
+
/** Returns a rectangle at the origin with the canvas's size. */
|
|
35
82
|
getBoundingClientRect(): {
|
|
83
|
+
/** Always 0. */
|
|
36
84
|
x: number;
|
|
85
|
+
/** Always 0. */
|
|
37
86
|
y: number;
|
|
87
|
+
/** The canvas width. */
|
|
38
88
|
width: number;
|
|
89
|
+
/** The canvas height. */
|
|
39
90
|
height: number;
|
|
91
|
+
/** Always 0. */
|
|
40
92
|
top: number;
|
|
93
|
+
/** Always 0. */
|
|
41
94
|
left: number;
|
|
42
95
|
};
|
|
96
|
+
/**
|
|
97
|
+
* Stores an attribute for `getAttribute()`.
|
|
98
|
+
* @param name - Attribute name.
|
|
99
|
+
* @param value - Attribute value.
|
|
100
|
+
*/
|
|
43
101
|
setAttribute(name: string, value: string): void;
|
|
102
|
+
/**
|
|
103
|
+
* Returns an attribute stored with `setAttribute()`, or `null` when unset.
|
|
104
|
+
* @param name - Attribute name.
|
|
105
|
+
*/
|
|
44
106
|
getAttribute(name: string): string | null;
|
|
107
|
+
/** Does nothing; the canvas dispatches no events. */
|
|
45
108
|
addEventListener(): void;
|
|
109
|
+
/** Does nothing. */
|
|
46
110
|
removeEventListener(): void;
|
|
111
|
+
/** Does nothing and returns `true`. */
|
|
47
112
|
dispatchEvent(): boolean;
|
|
48
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Creates a headless canvas whose `getContext('webgpu')` returns a texture-backed `GPUCanvasContext`. Also installed
|
|
116
|
+
* as a global in the test environment.
|
|
117
|
+
* @param width - Canvas width in pixels.
|
|
118
|
+
* @param height - Canvas height in pixels.
|
|
119
|
+
*/
|
|
49
120
|
export declare const createCanvas: (width: number, height: number) => HeadlessCanvas;
|
|
50
121
|
//#endregion
|
|
51
122
|
//#region src/index.d.ts
|
|
@@ -63,8 +134,15 @@ export type WebgpuNodeOptions = {
|
|
|
63
134
|
*/
|
|
64
135
|
export default class WebgpuEnvironment extends TestEnvironment {
|
|
65
136
|
#private;
|
|
137
|
+
/**
|
|
138
|
+
* Creates the environment; Jest calls this once per test file.
|
|
139
|
+
* @param config - Jest's environment config; `testEnvironmentOptions.dawnOptions` is read from it.
|
|
140
|
+
* @param context - Jest's environment context.
|
|
141
|
+
*/
|
|
66
142
|
constructor(config: JestEnvironmentConfig, context: EnvironmentContext);
|
|
143
|
+
/** Loads Dawn, then installs `navigator.gpu`, the `GPU*` globals, the DOM shims and `createCanvas` on the sandbox. */
|
|
67
144
|
setup(): Promise<void>;
|
|
145
|
+
/** Cancels pending animation frames and removes everything `setup()` installed from the sandbox. */
|
|
68
146
|
teardown(): Promise<void>;
|
|
69
147
|
}
|
|
70
148
|
//#endregion
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/canvas.ts","../src/index.ts"],"mappings":";;;KACY
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/canvas.ts","../src/index.ts"],"mappings":";;;KACY;;EAEV;;EAEA;;EAEA,MAAM;;;qBAUK;;;WAUA,QAAQ;;;;;EAFnB,YAEW;;EAAA,QAAQ;;;;;EAOnB,UAAU,QAAQ;;EAMlB;;EAMA,oBAAoB;;;;;EAQpB,qBAAqB;;;;;EAkBf,cAAc,QAAQ;;;qBAqCjB;;;EAaF;;EAEA;;WAZA,OAAO;;;;;;EAQhB,YAES;;EAAA,gBAEA;;EAAA;;MAIL;;MAKA;;;;;EAQJ,WAAW,eAAe;;;;;EAK1B,WAAW,aAAa;;EAMxB,cAAc,QAAQ;;EAKtB,aAAa;;EAKb;;IAEE;;IAEA;;IAEA;;IAEA;;IAEA;;IAEA;;;;;;;EAUF,aAAa,cAAc;;;;;EAQ3B,aAAa;;EAKb;;EAEA;;EAEA;;;;;;;;qBAWW,eAAY,eAAiB,mBAAmB;;;KCjNxD,wBAAwB,6BAA6B;KACrD,qBAAqB,6BAA6B;;YAK3C;;EAEV;;;;;;;qBAqDmB,0BAA0B;;;;;;;EAY7C,YAAY,QAAQ,uBAAuB,SAAS;;EA+BrC,SAAS;;EA4BT,YAAY"}
|
package/dist/index.mjs
CHANGED
|
@@ -13,20 +13,34 @@ var HeadlessCanvasContext = class {
|
|
|
13
13
|
canvas;
|
|
14
14
|
#config;
|
|
15
15
|
#texture;
|
|
16
|
+
/**
|
|
17
|
+
* Creates a context for a canvas; `HeadlessCanvas` does this for you.
|
|
18
|
+
* @param canvas - The canvas this context renders into; its current size sets the texture size.
|
|
19
|
+
*/
|
|
16
20
|
constructor(canvas) {
|
|
17
21
|
this.canvas = canvas;
|
|
18
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Stores the configuration and discards any current texture, as `GPUCanvasContext.configure()` does.
|
|
25
|
+
* @param config - The device, format and usage to render with.
|
|
26
|
+
*/
|
|
19
27
|
configure(config) {
|
|
20
28
|
this.#config = config;
|
|
21
29
|
this.#drop();
|
|
22
30
|
}
|
|
31
|
+
/** Clears the configuration and destroys the current texture. */
|
|
23
32
|
unconfigure() {
|
|
24
33
|
this.#config = void 0;
|
|
25
34
|
this.#drop();
|
|
26
35
|
}
|
|
36
|
+
/** Returns the configuration passed to `configure()`, or `null` before it is called or after `unconfigure()`. */
|
|
27
37
|
getConfiguration() {
|
|
28
38
|
return this.#config ?? null;
|
|
29
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Returns the texture to render into, creating it at the canvas's current size on first use or after a resize.
|
|
42
|
+
* @throws If `configure()` has not been called.
|
|
43
|
+
*/
|
|
30
44
|
getCurrentTexture() {
|
|
31
45
|
const config = this.#config;
|
|
32
46
|
if (!config) throw new Error("getCurrentTexture() called before configure()");
|
|
@@ -40,7 +54,10 @@ var HeadlessCanvasContext = class {
|
|
|
40
54
|
});
|
|
41
55
|
return this.#texture;
|
|
42
56
|
}
|
|
43
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Copies the current texture to the CPU. BGRA formats are swizzled to RGBA.
|
|
59
|
+
* @throws If the context is not configured or the format is not 8-bit RGBA or BGRA.
|
|
60
|
+
*/
|
|
44
61
|
async readPixels() {
|
|
45
62
|
const config = this.#config;
|
|
46
63
|
if (!config) throw new Error("readPixels() called before configure()");
|
|
@@ -85,21 +102,30 @@ var HeadlessCanvas = class {
|
|
|
85
102
|
width;
|
|
86
103
|
height;
|
|
87
104
|
#context = new HeadlessCanvasContext(this);
|
|
105
|
+
/** Style properties libraries may set; stored but not applied to anything. */
|
|
88
106
|
style = {};
|
|
89
107
|
#attributes = /* @__PURE__ */ new Map();
|
|
108
|
+
/**
|
|
109
|
+
* Creates a canvas of the given size; see also `createCanvas()`.
|
|
110
|
+
* @param width - Canvas width in pixels.
|
|
111
|
+
* @param height - Canvas height in pixels.
|
|
112
|
+
*/
|
|
90
113
|
constructor(width = 300, height = 150) {
|
|
91
114
|
this.width = width;
|
|
92
115
|
this.height = height;
|
|
93
116
|
}
|
|
117
|
+
/** Same as `width`. */
|
|
94
118
|
get clientWidth() {
|
|
95
119
|
return this.width;
|
|
96
120
|
}
|
|
121
|
+
/** Same as `height`. */
|
|
97
122
|
get clientHeight() {
|
|
98
123
|
return this.height;
|
|
99
124
|
}
|
|
100
125
|
getContext(id) {
|
|
101
126
|
return id === "webgpu" ? this.#context : null;
|
|
102
127
|
}
|
|
128
|
+
/** Reads back the canvas's current texture as RGBA8 pixels; see `HeadlessCanvasContext.readPixels()`. */
|
|
103
129
|
readPixels() {
|
|
104
130
|
return this.#context.readPixels();
|
|
105
131
|
}
|
|
@@ -107,6 +133,7 @@ var HeadlessCanvas = class {
|
|
|
107
133
|
asElement() {
|
|
108
134
|
return this;
|
|
109
135
|
}
|
|
136
|
+
/** Returns a rectangle at the origin with the canvas's size. */
|
|
110
137
|
getBoundingClientRect() {
|
|
111
138
|
return {
|
|
112
139
|
x: 0,
|
|
@@ -117,18 +144,36 @@ var HeadlessCanvas = class {
|
|
|
117
144
|
height: this.height
|
|
118
145
|
};
|
|
119
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Stores an attribute for `getAttribute()`.
|
|
149
|
+
* @param name - Attribute name.
|
|
150
|
+
* @param value - Attribute value.
|
|
151
|
+
*/
|
|
120
152
|
setAttribute(name, value) {
|
|
121
153
|
this.#attributes.set(name, value);
|
|
122
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Returns an attribute stored with `setAttribute()`, or `null` when unset.
|
|
157
|
+
* @param name - Attribute name.
|
|
158
|
+
*/
|
|
123
159
|
getAttribute(name) {
|
|
124
160
|
return this.#attributes.get(name) ?? null;
|
|
125
161
|
}
|
|
162
|
+
/** Does nothing; the canvas dispatches no events. */
|
|
126
163
|
addEventListener() {}
|
|
164
|
+
/** Does nothing. */
|
|
127
165
|
removeEventListener() {}
|
|
166
|
+
/** Does nothing and returns `true`. */
|
|
128
167
|
dispatchEvent() {
|
|
129
168
|
return true;
|
|
130
169
|
}
|
|
131
170
|
};
|
|
171
|
+
/**
|
|
172
|
+
* Creates a headless canvas whose `getContext('webgpu')` returns a texture-backed `GPUCanvasContext`. Also installed
|
|
173
|
+
* as a global in the test environment.
|
|
174
|
+
* @param width - Canvas width in pixels.
|
|
175
|
+
* @param height - Canvas height in pixels.
|
|
176
|
+
*/
|
|
132
177
|
const createCanvas = (width, height) => new HeadlessCanvas(width, height);
|
|
133
178
|
//#endregion
|
|
134
179
|
//#region src/index.ts
|
|
@@ -175,6 +220,11 @@ var WebgpuEnvironment = class extends TestEnvironment {
|
|
|
175
220
|
#nativeAdded;
|
|
176
221
|
#dawnOptions;
|
|
177
222
|
#animationFrames = /* @__PURE__ */ new Map();
|
|
223
|
+
/**
|
|
224
|
+
* Creates the environment; Jest calls this once per test file.
|
|
225
|
+
* @param config - Jest's environment config; `testEnvironmentOptions.dawnOptions` is read from it.
|
|
226
|
+
* @param context - Jest's environment context.
|
|
227
|
+
*/
|
|
178
228
|
constructor(config, context) {
|
|
179
229
|
const { dawnOptions, ...testEnvironmentOptions } = config.projectConfig.testEnvironmentOptions;
|
|
180
230
|
super({
|
|
@@ -193,6 +243,7 @@ var WebgpuEnvironment = class extends TestEnvironment {
|
|
|
193
243
|
enumerable: false
|
|
194
244
|
});
|
|
195
245
|
}
|
|
246
|
+
/** Loads Dawn, then installs `navigator.gpu`, the `GPU*` globals, the DOM shims and `createCanvas` on the sandbox. */
|
|
196
247
|
async setup() {
|
|
197
248
|
await super.setup();
|
|
198
249
|
const { create, globals } = await import("webgpu");
|
|
@@ -218,6 +269,7 @@ var WebgpuEnvironment = class extends TestEnvironment {
|
|
|
218
269
|
writable: true
|
|
219
270
|
});
|
|
220
271
|
}
|
|
272
|
+
/** Cancels pending animation frames and removes everything `setup()` installed from the sandbox. */
|
|
221
273
|
async teardown() {
|
|
222
274
|
const global = this.global;
|
|
223
275
|
for (const [handle, clear] of this.#animationFrames) clear(handle);
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +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;\nconst READABLE_FORMATS = new Set<GPUTextureFormat>(['rgba8unorm', 'rgba8unorm-srgb', 'bgra8unorm', 'bgra8unorm-srgb']);\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 config = this.#config;\n if (!config) throw new Error('readPixels() called before configure()');\n const { device, format } = config;\n if (!READABLE_FORMATS.has(format)) {\n throw new Error(`readPixels() supports only 8-bit RGBA/BGRA formats; received ${format}`);\n }\n const texture = this.getCurrentTexture();\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 try {\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 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 } finally {\n if (buffer.mapState === 'mapped') buffer.unmap();\n buffer.destroy();\n }\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. */\ntype TimerHandle = ReturnType<typeof setTimeout>;\ntype SandboxGlobal = Record<string, unknown> & {\n clearTimeout: typeof clearTimeout;\n performance: Performance;\n setTimeout: typeof setTimeout;\n};\n\nconst domShims = (\n global: SandboxGlobal,\n animationFrames: Map<TimerHandle, typeof clearTimeout>,\n): Record<string, unknown> => {\n const requestAnimationFrame = (callback: (time: number) => void): TimerHandle => {\n const handle = global.setTimeout(() => {\n animationFrames.delete(handle);\n callback(global.performance.now());\n }, 16);\n animationFrames.set(handle, global.clearTimeout);\n (handle as NodeJS.Timeout).unref?.();\n return handle;\n };\n const cancelAnimationFrame = (handle: TimerHandle): void => {\n const clear = animationFrames.get(handle) ?? global.clearTimeout;\n animationFrames.delete(handle);\n clear(handle);\n };\n const 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 return {\n document,\n devicePixelRatio: 1,\n requestAnimationFrame,\n cancelAnimationFrame,\n addEventListener() {},\n removeEventListener() {},\n HTMLCanvasElement: HeadlessCanvas,\n window: global,\n self: global,\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 #hostNavigator: PropertyDescriptor | undefined;\n #nativeAdded: boolean;\n #dawnOptions: string[] | undefined;\n #animationFrames = new Map<TimerHandle, typeof clearTimeout>();\n\n constructor(config: JestEnvironmentConfig, context: EnvironmentContext) {\n // jest-environment-node assigns every `testEnvironmentOptions` entry onto the sandbox global, so\n // strip this environment's own option first to keep `dawnOptions` off `globalThis`.\n const { dawnOptions, ...testEnvironmentOptions } = config.projectConfig.testEnvironmentOptions;\n super({ ...config, projectConfig: { ...config.projectConfig, testEnvironmentOptions } }, context);\n this.#dawnOptions = dawnOptions as string[] | undefined;\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 }\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 SandboxGlobal;\n const shims = { ...globals, ...domShims(global, this.#animationFrames), 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 // jest-environment-node hands the sandbox Node's own `navigator` by reference, so adding `gpu` to it\n // would leak onto the host process and be shared by every environment in this worker. Give the\n // sandbox its own proxy instead; `Reflect.get(target, key, target)` keeps Node's private-field\n // getters (hardwareConcurrency, userAgent, ...) working through it.\n const gpu = create(this.#dawnOptions ?? []);\n const host = (global.navigator ?? {}) as object;\n this.#hostNavigator = Object.getOwnPropertyDescriptor(global, 'navigator');\n Object.defineProperty(global, 'navigator', {\n value: new Proxy(host, {\n get: (target, key) => (key === 'gpu' ? gpu : Reflect.get(target, key, target)),\n has: (target, key) => key === 'gpu' || Reflect.has(target, key),\n }),\n configurable: true,\n enumerable: true,\n writable: true,\n });\n }\n\n override async teardown(): Promise<void> {\n const global = this.global as unknown as Record<string, unknown>;\n for (const [handle, clear] of this.#animationFrames) clear(handle);\n this.#animationFrames.clear();\n if (this.#hostNavigator) Object.defineProperty(global, 'navigator', this.#hostNavigator);\n else delete global.navigator;\n for (const key of this.#added) delete global[key];\n await super.teardown();\n }\n}\n"],"mappings":"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"}
|
|
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 = {\n /** Width in pixels. */\n width: number;\n /** Height in pixels. */\n height: number;\n /** Pixel bytes, four per pixel in R, G, B, A order, rows top to bottom with no padding. */\n data: Uint8Array;\n};\n\nconst COPY_SRC = 0x01;\nconst RENDER_ATTACHMENT = 0x10;\nconst MAP_READ = 0x01;\nconst COPY_DST = 0x08;\nconst READABLE_FORMATS = new Set<GPUTextureFormat>(['rgba8unorm', 'rgba8unorm-srgb', 'bgra8unorm', 'bgra8unorm-srgb']);\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 /**\n * Creates a context for a canvas; `HeadlessCanvas` does this for you.\n * @param canvas - The canvas this context renders into; its current size sets the texture size.\n */\n constructor(\n /** The canvas that owns this context. */\n readonly canvas: HeadlessCanvas,\n ) {}\n\n /**\n * Stores the configuration and discards any current texture, as `GPUCanvasContext.configure()` does.\n * @param config - The device, format and usage to render with.\n */\n configure(config: GPUCanvasConfiguration): void {\n this.#config = config;\n this.#drop();\n }\n\n /** Clears the configuration and destroys the current texture. */\n unconfigure(): void {\n this.#config = undefined;\n this.#drop();\n }\n\n /** Returns the configuration passed to `configure()`, or `null` before it is called or after `unconfigure()`. */\n getConfiguration(): GPUCanvasConfiguration | null {\n return this.#config ?? null;\n }\n\n /**\n * Returns the texture to render into, creating it at the canvas's current size on first use or after a resize.\n * @throws If `configure()` has not been called.\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 /**\n * Copies the current texture to the CPU. BGRA formats are swizzled to RGBA.\n * @throws If the context is not configured or the format is not 8-bit RGBA or BGRA.\n */\n async readPixels(): Promise<RgbaImage> {\n const config = this.#config;\n if (!config) throw new Error('readPixels() called before configure()');\n const { device, format } = config;\n if (!READABLE_FORMATS.has(format)) {\n throw new Error(`readPixels() supports only 8-bit RGBA/BGRA formats; received ${format}`);\n }\n const texture = this.getCurrentTexture();\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 try {\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 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 } finally {\n if (buffer.mapState === 'mapped') buffer.unmap();\n buffer.destroy();\n }\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 /** Style properties libraries may set; stored but not applied to anything. */\n readonly style: Record<string, string> = {};\n readonly #attributes = new Map<string, string>();\n\n /**\n * Creates a canvas of the given size; see also `createCanvas()`.\n * @param width - Canvas width in pixels.\n * @param height - Canvas height in pixels.\n */\n constructor(\n /** Canvas width in pixels. Change it to resize the render target. */\n public width = 300,\n /** Canvas height in pixels. Change it to resize the render target. */\n public height = 150,\n ) {}\n\n /** Same as `width`. */\n get clientWidth(): number {\n return this.width;\n }\n\n /** Same as `height`. */\n get clientHeight(): number {\n return this.height;\n }\n\n /**\n * Returns this canvas's WebGPU context.\n * @param id - The context type, `'webgpu'`.\n */\n getContext(id: 'webgpu'): HeadlessCanvasContext;\n /**\n * Returns this canvas's WebGPU context for `'webgpu'`, or `null` for any other type.\n * @param id - The context type requested.\n */\n getContext(id: string): HeadlessCanvasContext | null;\n getContext(id: string): HeadlessCanvasContext | null {\n return id === 'webgpu' ? this.#context : null;\n }\n\n /** Reads back the canvas's current texture as RGBA8 pixels; see `HeadlessCanvasContext.readPixels()`. */\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 /** Returns a rectangle at the origin with the canvas's size. */\n getBoundingClientRect(): {\n /** Always 0. */\n x: number;\n /** Always 0. */\n y: number;\n /** The canvas width. */\n width: number;\n /** The canvas height. */\n height: number;\n /** Always 0. */\n top: number;\n /** Always 0. */\n left: number;\n } {\n return { x: 0, y: 0, top: 0, left: 0, width: this.width, height: this.height };\n }\n\n /**\n * Stores an attribute for `getAttribute()`.\n * @param name - Attribute name.\n * @param value - Attribute value.\n */\n setAttribute(name: string, value: string): void {\n this.#attributes.set(name, value);\n }\n\n /**\n * Returns an attribute stored with `setAttribute()`, or `null` when unset.\n * @param name - Attribute name.\n */\n getAttribute(name: string): string | null {\n return this.#attributes.get(name) ?? null;\n }\n\n /** Does nothing; the canvas dispatches no events. */\n addEventListener(): void {}\n /** Does nothing. */\n removeEventListener(): void {}\n /** Does nothing and returns `true`. */\n dispatchEvent(): boolean {\n return true;\n }\n}\n\n/**\n * Creates a headless canvas whose `getContext('webgpu')` returns a texture-backed `GPUCanvasContext`. Also installed\n * as a global in the test environment.\n * @param width - Canvas width in pixels.\n * @param height - Canvas height in pixels.\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. */\ntype TimerHandle = ReturnType<typeof setTimeout>;\ntype SandboxGlobal = Record<string, unknown> & {\n clearTimeout: typeof clearTimeout;\n performance: Performance;\n setTimeout: typeof setTimeout;\n};\n\nconst domShims = (\n global: SandboxGlobal,\n animationFrames: Map<TimerHandle, typeof clearTimeout>,\n): Record<string, unknown> => {\n const requestAnimationFrame = (callback: (time: number) => void): TimerHandle => {\n const handle = global.setTimeout(() => {\n animationFrames.delete(handle);\n callback(global.performance.now());\n }, 16);\n animationFrames.set(handle, global.clearTimeout);\n handle.unref?.();\n return handle;\n };\n const cancelAnimationFrame = (handle: TimerHandle): void => {\n const clear = animationFrames.get(handle) ?? global.clearTimeout;\n animationFrames.delete(handle);\n clear(handle);\n };\n const 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 return {\n document,\n devicePixelRatio: 1,\n requestAnimationFrame,\n cancelAnimationFrame,\n addEventListener() {},\n removeEventListener() {},\n HTMLCanvasElement: HeadlessCanvas,\n window: global,\n self: global,\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 #hostNavigator: PropertyDescriptor | undefined;\n #nativeAdded: boolean;\n #dawnOptions: string[] | undefined;\n #animationFrames = new Map<TimerHandle, typeof clearTimeout>();\n\n /**\n * Creates the environment; Jest calls this once per test file.\n * @param config - Jest's environment config; `testEnvironmentOptions.dawnOptions` is read from it.\n * @param context - Jest's environment context.\n */\n constructor(config: JestEnvironmentConfig, context: EnvironmentContext) {\n // jest-environment-node assigns every `testEnvironmentOptions` entry onto the sandbox global, so\n // strip this environment's own option first to keep `dawnOptions` off `globalThis`.\n const { dawnOptions, ...testEnvironmentOptions } = config.projectConfig.testEnvironmentOptions;\n super({ ...config, projectConfig: { ...config.projectConfig, testEnvironmentOptions } }, context);\n this.#dawnOptions = dawnOptions as string[] | undefined;\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 }\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 /** Loads Dawn, then installs `navigator.gpu`, the `GPU*` globals, the DOM shims and `createCanvas` on the sandbox. */\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 SandboxGlobal;\n // oxlint-disable-next-line typescript/no-misused-spread -- intentionally copies the own enumerable GPU* globals\n const shims = { ...globals, ...domShims(global, this.#animationFrames), 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 // jest-environment-node hands the sandbox Node's own `navigator` by reference, so adding `gpu` to it\n // would leak onto the host process and be shared by every environment in this worker. Give the\n // sandbox its own proxy instead; `Reflect.get(target, key, target)` keeps Node's private-field\n // getters (hardwareConcurrency, userAgent, ...) working through it.\n const gpu = create(this.#dawnOptions ?? []);\n const host = global.navigator ?? {};\n this.#hostNavigator = Object.getOwnPropertyDescriptor(global, 'navigator');\n Object.defineProperty(global, 'navigator', {\n value: new Proxy(host, {\n get: (target, key) => (key === 'gpu' ? gpu : Reflect.get(target, key, target)),\n has: (target, key) => key === 'gpu' || Reflect.has(target, key),\n }),\n configurable: true,\n enumerable: true,\n writable: true,\n });\n }\n\n /** Cancels pending animation frames and removes everything `setup()` installed from the sandbox. */\n override async teardown(): Promise<void> {\n const global = this.global as unknown as Record<string, unknown>;\n for (const [handle, clear] of this.#animationFrames) clear(handle);\n this.#animationFrames.clear();\n if (this.#hostNavigator) Object.defineProperty(global, 'navigator', this.#hostNavigator);\n else delete global.navigator;\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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jest-environment-webgpu-node",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Jest environment that provides real headless WebGPU (Dawn via the webgpu package) in Node",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dawn",
|
|
@@ -30,8 +30,7 @@
|
|
|
30
30
|
"dist/**/*.d.cts.map",
|
|
31
31
|
"dist/**/*.d.mts",
|
|
32
32
|
"dist/**/*.d.mts.map",
|
|
33
|
-
"globals.d.ts"
|
|
34
|
-
"CHANGELOG.md"
|
|
33
|
+
"globals.d.ts"
|
|
35
34
|
],
|
|
36
35
|
"type": "module",
|
|
37
36
|
"main": "./dist/index.cjs",
|
|
@@ -55,10 +54,6 @@
|
|
|
55
54
|
"publishConfig": {
|
|
56
55
|
"access": "public"
|
|
57
56
|
},
|
|
58
|
-
"scripts": {
|
|
59
|
-
"build": "tsdown",
|
|
60
|
-
"tsc": "tsc --noEmit"
|
|
61
|
-
},
|
|
62
57
|
"dependencies": {
|
|
63
58
|
"@webgpu/types": "^0.1.72",
|
|
64
59
|
"webgpu": "^0.6.1"
|
|
@@ -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-webgpu-node-v0.1.2](https://github.com/bhouston/jest-gpu/compare/jest-environment-webgpu-node-v0.1.1...jest-environment-webgpu-node-v0.1.2) (2026-09-21)
|
|
2
|
-
|
|
3
|
-
### Bug Fixes
|
|
4
|
-
|
|
5
|
-
* **webgpu:** give the sandbox its own navigator and keep dawnOptions off the global ([34ff66c](https://github.com/bhouston/jest-gpu/commit/34ff66c360b602740198da938ee32d34bbd7b783)), closes [#21](https://github.com/bhouston/jest-gpu/issues/21)
|