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 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
@@ -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);
@@ -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
- 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;EA8BrC,SAAS;EA0BT,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;EA8BrC,SAAS;EA0BT,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
@@ -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);
@@ -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.2",
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.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.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)