jest-environment-webgpu-node 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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, 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.
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
- 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.
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
- ## Author
113
+ ## Contribution
81
114
 
82
- Created by [Ben Houston](https://ben3d.ca) and sponsored by [Land of Assets](https://landofassets.com).
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
- /** Copies the current texture to the CPU. BGRA formats are swizzled to RGBA. */
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
@@ -175,12 +220,25 @@ const domShims = (global, animationFrames) => {
175
220
  */
176
221
  var WebgpuEnvironment = class extends jest_environment_node.TestEnvironment {
177
222
  #added = [];
178
- #navigator = {};
223
+ #hostNavigator;
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
- super(config, context);
233
+ const { dawnOptions, ...testEnvironmentOptions } = config.projectConfig.testEnvironmentOptions;
234
+ super({
235
+ ...config,
236
+ projectConfig: {
237
+ ...config.projectConfig,
238
+ testEnvironmentOptions
239
+ }
240
+ }, context);
241
+ this.#dawnOptions = dawnOptions;
184
242
  this.#nativeAdded = !Object.getOwnPropertyNames(this.global).includes("_native");
185
243
  if (this.#nativeAdded) Object.defineProperty(this.global, "_native", {
186
244
  value: void 0,
@@ -188,8 +246,8 @@ var WebgpuEnvironment = class extends jest_environment_node.TestEnvironment {
188
246
  configurable: true,
189
247
  enumerable: false
190
248
  });
191
- this.#dawnOptions = config.projectConfig.testEnvironmentOptions.dawnOptions;
192
249
  }
250
+ /** Loads Dawn, then installs `navigator.gpu`, the `GPU*` globals, the DOM shims and `createCanvas` on the sandbox. */
193
251
  async setup() {
194
252
  await super.setup();
195
253
  const { create, globals } = await import("webgpu");
@@ -202,17 +260,26 @@ var WebgpuEnvironment = class extends jest_environment_node.TestEnvironment {
202
260
  this.#added = Object.keys(shims).filter((key) => !(key in global));
203
261
  for (const key of this.#added) global[key] = shims[key];
204
262
  if (this.#nativeAdded) this.#added.push("_native");
205
- this.#navigator = global.navigator ??= {};
206
- Object.defineProperty(this.#navigator, "gpu", {
207
- value: create(this.#dawnOptions ?? []),
208
- configurable: true
263
+ const gpu = create(this.#dawnOptions ?? []);
264
+ const host = global.navigator ?? {};
265
+ this.#hostNavigator = Object.getOwnPropertyDescriptor(global, "navigator");
266
+ Object.defineProperty(global, "navigator", {
267
+ value: new Proxy(host, {
268
+ get: (target, key) => key === "gpu" ? gpu : Reflect.get(target, key, target),
269
+ has: (target, key) => key === "gpu" || Reflect.has(target, key)
270
+ }),
271
+ configurable: true,
272
+ enumerable: true,
273
+ writable: true
209
274
  });
210
275
  }
276
+ /** Cancels pending animation frames and removes everything `setup()` installed from the sandbox. */
211
277
  async teardown() {
212
278
  const global = this.global;
213
279
  for (const [handle, clear] of this.#animationFrames) clear(handle);
214
280
  this.#animationFrames.clear();
215
- delete this.#navigator.gpu;
281
+ if (this.#hostNavigator) Object.defineProperty(global, "navigator", this.#hostNavigator);
282
+ else delete global.navigator;
216
283
  for (const key of this.#added) delete global[key];
217
284
  await super.teardown();
218
285
  }
@@ -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 #navigator: { gpu?: GPU } = {};\n #nativeAdded: boolean;\n #dawnOptions: string[] | undefined;\n #animationFrames = new Map<TimerHandle, typeof clearTimeout>();\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 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 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 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 delete this.#navigator.gpu;\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
- constructor(canvas: HeadlessCanvas);
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
- /** Copies the current texture to the CPU. BGRA formats are swizzled to RGBA. */
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
- constructor(width?: number, height?: number);
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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../src/canvas.ts","../src/index.ts"],"mappings":";;;KACY;EAAc;EAAe;EAAgB,MAAM;;;qBASlD;;WAIU,QAAQ;EAA7B,YAAqB,QAAQ;EAE7B,UAAU,QAAQ;EAKlB;EAKA,oBAAoB;EAIpB,qBAAqB;;EAef,cAAc,QAAQ;;;qBAqCjB;;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;;;KCnIxD,wBAAwB,6BAA6B;KACrD,qBAAqB,6BAA6B;;YAK3C;;EAEV;;;;;;;qBAqDmB,0BAA0B;;EAO7C,YAAY,QAAQ,uBAAuB,SAAS;EA2BrC,SAAS;EAYT,YAAY"}
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
- constructor(canvas: HeadlessCanvas);
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
- /** Copies the current texture to the CPU. BGRA formats are swizzled to RGBA. */
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
- constructor(width?: number, height?: number);
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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/canvas.ts","../src/index.ts"],"mappings":";;;KACY;EAAc;EAAe;EAAgB,MAAM;;;qBASlD;;WAIU,QAAQ;EAA7B,YAAqB,QAAQ;EAE7B,UAAU,QAAQ;EAKlB;EAKA,oBAAoB;EAIpB,qBAAqB;;EAef,cAAc,QAAQ;;;qBAqCjB;;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;;;KCnIxD,wBAAwB,6BAA6B;KACrD,qBAAqB,6BAA6B;;YAK3C;;EAEV;;;;;;;qBAqDmB,0BAA0B;;EAO7C,YAAY,QAAQ,uBAAuB,SAAS;EA2BrC,SAAS;EAYT,YAAY"}
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
- /** Copies the current texture to the CPU. BGRA formats are swizzled to RGBA. */
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
@@ -171,12 +216,25 @@ const domShims = (global, animationFrames) => {
171
216
  */
172
217
  var WebgpuEnvironment = class extends TestEnvironment {
173
218
  #added = [];
174
- #navigator = {};
219
+ #hostNavigator;
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
- super(config, context);
229
+ const { dawnOptions, ...testEnvironmentOptions } = config.projectConfig.testEnvironmentOptions;
230
+ super({
231
+ ...config,
232
+ projectConfig: {
233
+ ...config.projectConfig,
234
+ testEnvironmentOptions
235
+ }
236
+ }, context);
237
+ this.#dawnOptions = dawnOptions;
180
238
  this.#nativeAdded = !Object.getOwnPropertyNames(this.global).includes("_native");
181
239
  if (this.#nativeAdded) Object.defineProperty(this.global, "_native", {
182
240
  value: void 0,
@@ -184,8 +242,8 @@ var WebgpuEnvironment = class extends TestEnvironment {
184
242
  configurable: true,
185
243
  enumerable: false
186
244
  });
187
- this.#dawnOptions = config.projectConfig.testEnvironmentOptions.dawnOptions;
188
245
  }
246
+ /** Loads Dawn, then installs `navigator.gpu`, the `GPU*` globals, the DOM shims and `createCanvas` on the sandbox. */
189
247
  async setup() {
190
248
  await super.setup();
191
249
  const { create, globals } = await import("webgpu");
@@ -198,17 +256,26 @@ var WebgpuEnvironment = class extends TestEnvironment {
198
256
  this.#added = Object.keys(shims).filter((key) => !(key in global));
199
257
  for (const key of this.#added) global[key] = shims[key];
200
258
  if (this.#nativeAdded) this.#added.push("_native");
201
- this.#navigator = global.navigator ??= {};
202
- Object.defineProperty(this.#navigator, "gpu", {
203
- value: create(this.#dawnOptions ?? []),
204
- configurable: true
259
+ const gpu = create(this.#dawnOptions ?? []);
260
+ const host = global.navigator ?? {};
261
+ this.#hostNavigator = Object.getOwnPropertyDescriptor(global, "navigator");
262
+ Object.defineProperty(global, "navigator", {
263
+ value: new Proxy(host, {
264
+ get: (target, key) => key === "gpu" ? gpu : Reflect.get(target, key, target),
265
+ has: (target, key) => key === "gpu" || Reflect.has(target, key)
266
+ }),
267
+ configurable: true,
268
+ enumerable: true,
269
+ writable: true
205
270
  });
206
271
  }
272
+ /** Cancels pending animation frames and removes everything `setup()` installed from the sandbox. */
207
273
  async teardown() {
208
274
  const global = this.global;
209
275
  for (const [handle, clear] of this.#animationFrames) clear(handle);
210
276
  this.#animationFrames.clear();
211
- delete this.#navigator.gpu;
277
+ if (this.#hostNavigator) Object.defineProperty(global, "navigator", this.#hostNavigator);
278
+ else delete global.navigator;
212
279
  for (const key of this.#added) delete global[key];
213
280
  await super.teardown();
214
281
  }
@@ -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 #navigator: { gpu?: GPU } = {};\n #nativeAdded: boolean;\n #dawnOptions: string[] | undefined;\n #animationFrames = new Map<TimerHandle, typeof clearTimeout>();\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 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 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 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 delete this.#navigator.gpu;\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.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
- "src/globals.d.ts",
34
- "CHANGELOG.md"
33
+ "globals.d.ts"
35
34
  ],
36
35
  "type": "module",
37
36
  "main": "./dist/index.cjs",
@@ -49,16 +48,12 @@
49
48
  }
50
49
  },
51
50
  "./globals": {
52
- "types": "./src/globals.d.ts"
51
+ "types": "./globals.d.ts"
53
52
  }
54
53
  },
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.12.0"
66
+ "node": ">=22"
67
+ },
68
+ "scripts": {
69
+ "build": "tsdown",
70
+ "tsc": "tsc --noEmit"
72
71
  }
73
- }
72
+ }
package/CHANGELOG.md DELETED
@@ -1,5 +0,0 @@
1
- ## [jest-environment-webgpu-node-v0.1.1](https://github.com/bhouston/jest-gpu/compare/jest-environment-webgpu-node-v0.1.0...jest-environment-webgpu-node-v0.1.1) (2026-09-21)
2
-
3
- ### Bug Fixes
4
-
5
- * **webgpu:** correct sandbox shims and pixel reads ([92c4118](https://github.com/bhouston/jest-gpu/commit/92c4118be3b36f30bb7a8b397de4e09d02035ab6)), closes [#19](https://github.com/bhouston/jest-gpu/issues/19)
File without changes