• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / luma.gl / 28177465729

25 Jun 2026 02:28PM UTC coverage: 70.408% (-0.3%) from 70.66%
28177465729

push

github

web-flow
website: Improve example browsing and InfoBox panels (#2692)

9602 of 15462 branches covered (62.1%)

Branch coverage included in aggregate %.

19228 of 25485 relevant lines covered (75.45%)

4245.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.82
/modules/test-utils/src/create-test-device.ts
1
// luma.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import type {Device, CanvasContextProps, DeviceProps} from '@luma.gl/core';
6
import {luma, log} from '@luma.gl/core';
7
import {webgl2Adapter, WebGLDevice} from '@luma.gl/webgl';
8
import {webgpuAdapter, WebGPUDevice} from '@luma.gl/webgpu';
9
import {nullAdapter} from './null-device/null-adapter';
10
import {NullDevice} from './null-device/null-device';
11

12
const DEFAULT_CANVAS_CONTEXT_PROPS: CanvasContextProps = {width: 1, height: 1};
107✔
13
const TEST_DEVICE_CACHE_KEY = '__lumaTestDeviceCache';
107✔
14

15
type TestDeviceCache = {
16
  /** A null device intended for testing - @note Only available after getTestDevices() has completed */
17
  nullDevicePromise: Promise<NullDevice> | null;
18
  /** This WebGL Device can be used directly but will not have WebGL debugging initialized */
19
  webglDevicePromise: Promise<WebGLDevice> | null;
20
  /** A shared offscreen WebGL device for presentation-context tests */
21
  presentationWebglDevicePromise: Promise<WebGLDevice | null> | null;
22
  /** WebGPU Devices intended for testing, keyed by featureLevel */
23
  webgpuDevicePromises: Partial<
24
    Record<NonNullable<DeviceProps['featureLevel']>, Promise<WebGPUDevice | null>>
25
  >;
26
};
27

28
declare global {
29
  interface Window {
30
    [TEST_DEVICE_CACHE_KEY]?: TestDeviceCache;
31
  }
32
}
33

34
const testDeviceCache = getOrCreateTestDeviceCache();
107✔
35

36
type LostAwareDevice = {
37
  isLost: boolean;
38
};
39

40
type TestDeviceType =
41
  | 'webgl'
42
  | 'webgpu'
43
  | 'webgpu-core'
44
  | 'webgpu-max'
45
  | 'webgpu-compatibility'
46
  | 'null'
47
  | 'unknown';
48

49
/**
50
 * Returns available test devices for the requested backend types.
51
 * @param types Backend types to create. `'webgpu'` preserves the legacy max-feature WebGPU test device.
52
 */
53
export async function getTestDevices(
54
  types: Readonly<TestDeviceType[]> = ['webgl', 'webgpu']
61✔
55
): Promise<Device[]> {
56
  const promises = types.map(type => getTestDevice(type));
179✔
57
  const devices = await Promise.all(promises);
83✔
58
  return devices.filter(device => device !== null);
179✔
59
}
60

61
/**
62
 * Returns a test device for one backend type, or `null` when that backend is unavailable.
63
 * @param type Backend type to create.
64
 */
65
export async function getTestDevice(type: TestDeviceType): Promise<Device | null> {
66
  switch (type) {
382!
67
    case 'webgl':
68
      return getOrCreateWebGLTestDevicePromise();
155✔
69
    case 'webgpu':
70
      return getWebGPUTestDevice('max');
143✔
71
    case 'webgpu-core':
72
      return getWebGPUTestDevice('core');
×
73
    case 'webgpu-max':
74
      return getWebGPUTestDevice('max');
×
75
    case 'webgpu-compatibility':
76
      return getWebGPUTestDevice('compatibility');
×
77
    case 'null':
78
      return getOrCreateNullTestDevicePromise();
84✔
79
    case 'unknown':
80
      return null;
×
81
  }
82
}
83

84
/**
85
 * Returns a WebGPU test device for one feature level, or `null` when WebGPU is unavailable.
86
 * @param featureLevel WebGPU feature level to request. Defaults to `'max'` for existing tests.
87
 */
88
export async function getWebGPUTestDevice(
89
  featureLevel: NonNullable<DeviceProps['featureLevel']> = 'max'
87✔
90
): Promise<WebGPUDevice | null> {
91
  return _refreshLostCachedTestDevice(
234✔
92
    () => getOrCreateWebGPUTestDevicePromise(featureLevel),
234✔
93
    () => {
94
      delete testDeviceCache.webgpuDevicePromises[featureLevel];
×
95
    }
96
  );
97
}
98

99
/**
100
 * Returns available WebGPU test devices for the requested feature levels.
101
 * @param featureLevels WebGPU feature levels to request. Defaults to both `'core'` and `'max'`.
102
 */
103
export async function getWebGPUTestDevices(
104
  featureLevels: Readonly<NonNullable<DeviceProps['featureLevel']>[]> = ['core', 'max']
2✔
105
): Promise<WebGPUDevice[]> {
106
  const devices = await Promise.all(
2✔
107
    featureLevels.map(featureLevel => getWebGPUTestDevice(featureLevel))
4✔
108
  );
109

110
  return devices.filter((device): device is WebGPUDevice => device !== null);
4✔
111
}
112

113
/** returns WebGL device promise, if available */
114
export async function getWebGLTestDevice(): Promise<WebGLDevice> {
115
  return _refreshLostCachedTestDevice(getOrCreateWebGLTestDevicePromise, () => {
179✔
116
    testDeviceCache.webglDevicePromise = null;
×
117
  });
118
}
119

120
/** returns an offscreen WebGL device promise for presentation-context tests, if available */
121
export async function getPresentationWebGLTestDevice(): Promise<WebGLDevice | null> {
122
  return getOrCreatePresentationWebGLTestDevicePromise();
5✔
123
}
124

125
/** returns null device promise, if available */
126
export async function getNullTestDevice(): Promise<NullDevice> {
127
  return getOrCreateNullTestDevicePromise();
22✔
128
}
129

130
function getOrCreateWebGPUTestDevicePromise(
131
  featureLevel: NonNullable<DeviceProps['featureLevel']>
132
): Promise<WebGPUDevice | null> {
133
  testDeviceCache.webgpuDevicePromises[featureLevel] ||= makeWebGPUTestDevice(featureLevel);
234✔
134
  return testDeviceCache.webgpuDevicePromises[featureLevel];
234✔
135
}
136

137
function getOrCreateWebGLTestDevicePromise(): Promise<WebGLDevice> {
138
  testDeviceCache.webglDevicePromise ||= makeWebGLTestDevice();
334✔
139
  return testDeviceCache.webglDevicePromise;
334✔
140
}
141

142
function getOrCreatePresentationWebGLTestDevicePromise(): Promise<WebGLDevice | null> {
143
  testDeviceCache.presentationWebglDevicePromise ||= makePresentationWebGLTestDevice();
5✔
144
  return testDeviceCache.presentationWebglDevicePromise;
5✔
145
}
146

147
function getOrCreateNullTestDevicePromise(): Promise<NullDevice> {
148
  testDeviceCache.nullDevicePromise ||= makeNullTestDevice();
106✔
149
  return testDeviceCache.nullDevicePromise;
106✔
150
}
151

152
async function makeWebGPUTestDevice(
153
  featureLevel: NonNullable<DeviceProps['featureLevel']>
154
): Promise<WebGPUDevice | null> {
155
  const webgpuDeviceResolvers = withResolvers<WebGPUDevice | null>();
59✔
156
  try {
59✔
157
    const webgpuDevice = (await luma.createDevice({
59✔
158
      id: `webgpu-${featureLevel}-test-device`,
159
      type: 'webgpu',
160
      featureLevel,
161
      adapters: [webgpuAdapter],
162
      createCanvasContext: DEFAULT_CANVAS_CONTEXT_PROPS,
163
      debug: true
164
    })) as unknown as WebGPUDevice;
165
    webgpuDevice.lost.finally(() => {
59✔
166
      if (testDeviceCache.webgpuDevicePromises[featureLevel] === webgpuDeviceResolvers.promise) {
6!
167
        delete testDeviceCache.webgpuDevicePromises[featureLevel];
×
168
      }
169
    });
170
    webgpuDeviceResolvers.resolve(webgpuDevice);
59✔
171
  } catch (error) {
172
    log.error(String(error))();
×
173
    // @ts-ignore TODO
174
    webgpuDeviceResolvers.resolve(null);
×
175
  }
176
  return webgpuDeviceResolvers.promise;
59✔
177
}
178

179
/** returns WebGL device promise, if available */
180
async function makeWebGLTestDevice(): Promise<WebGLDevice> {
181
  const webglDeviceResolvers = withResolvers<WebGLDevice>();
73✔
182
  try {
73✔
183
    const webglDevice = (await luma.createDevice({
73✔
184
      id: 'webgl-test-device',
185
      type: 'webgl',
186
      adapters: [webgl2Adapter],
187
      createCanvasContext: DEFAULT_CANVAS_CONTEXT_PROPS,
188
      debug: true
189
    })) as unknown as WebGLDevice;
190
    webglDevice.lost.finally(() => {
73✔
191
      if (testDeviceCache.webglDevicePromise === webglDeviceResolvers.promise) {
3!
192
        testDeviceCache.webglDevicePromise = null;
×
193
      }
194
    });
195
    webglDeviceResolvers.resolve(webglDevice);
73✔
196
  } catch (error) {
197
    log.error(String(error))();
×
198
    // @ts-ignore TODO
199
    webglDeviceResolvers.resolve(null);
×
200
  }
201
  return webglDeviceResolvers.promise;
73✔
202
}
203

204
async function makePresentationWebGLTestDevice(): Promise<WebGLDevice | null> {
205
  if (typeof OffscreenCanvas === 'undefined') {
1!
206
    return null;
×
207
  }
208

209
  const presentationWebGLDeviceResolvers = withResolvers<WebGLDevice | null>();
1✔
210
  try {
1✔
211
    const webglDevice = (await luma.createDevice({
1✔
212
      id: 'webgl-presentation-context-test-device',
213
      type: 'webgl',
214
      adapters: [webgl2Adapter],
215
      createCanvasContext: {canvas: new OffscreenCanvas(4, 4)},
216
      debug: true
217
    })) as unknown as WebGLDevice;
218
    webglDevice.lost.finally(() => {
1✔
219
      if (
×
220
        testDeviceCache.presentationWebglDevicePromise === presentationWebGLDeviceResolvers.promise
221
      ) {
222
        testDeviceCache.presentationWebglDevicePromise = null;
×
223
      }
224
    });
225
    presentationWebGLDeviceResolvers.resolve(webglDevice);
1✔
226
  } catch (error) {
227
    log.error(String(error))();
×
228
    presentationWebGLDeviceResolvers.resolve(null);
×
229
  }
230
  return presentationWebGLDeviceResolvers.promise;
1✔
231
}
232

233
/** returns null device promise, if available */
234
async function makeNullTestDevice(): Promise<NullDevice> {
235
  const nullDeviceResolvers = withResolvers<NullDevice>();
30✔
236
  try {
30✔
237
    const nullDevice = (await luma.createDevice({
30✔
238
      id: 'null-test-device',
239
      type: 'null',
240
      adapters: [nullAdapter],
241
      createCanvasContext: DEFAULT_CANVAS_CONTEXT_PROPS,
242
      debug: true
243
    })) as unknown as NullDevice;
244
    nullDeviceResolvers.resolve(nullDevice);
30✔
245
  } catch (error) {
246
    log.error(String(error))();
×
247
    // @ts-ignore TODO
248
    testDeviceCache.nullDevicePromise = Promise.resolve(null);
×
249
  }
250
  return nullDeviceResolvers.promise;
30✔
251
}
252

253
// HELPERS
254

255
export async function _refreshLostCachedTestDevice<DeviceT extends LostAwareDevice | null>(
256
  getOrCreateDevice: () => Promise<DeviceT>,
257
  clearCachedDevice: () => void
258
): Promise<DeviceT> {
259
  const device = await getOrCreateDevice();
414✔
260
  if (device?.isLost) {
414✔
261
    clearCachedDevice();
1✔
262
    return getOrCreateDevice();
1✔
263
  }
264
  return device;
413✔
265
}
266

267
function getOrCreateTestDeviceCache(): TestDeviceCache {
268
  const rootObject = globalThis as typeof globalThis & {
107✔
269
    [TEST_DEVICE_CACHE_KEY]?: TestDeviceCache;
270
  };
271

272
  rootObject[TEST_DEVICE_CACHE_KEY] ||= {
107✔
273
    nullDevicePromise: null,
274
    webglDevicePromise: null,
275
    presentationWebglDevicePromise: null,
276
    webgpuDevicePromises: {}
277
  };
278

279
  return rootObject[TEST_DEVICE_CACHE_KEY];
107✔
280
}
281

282
// TODO - replace with Promise.withResolvers once we upgrade TS baseline
283
function withResolvers<T>(): {
284
  promise: Promise<T>;
285
  resolve: (t: T) => void;
286
  reject: (error: Error) => void;
287
} {
288
  let resolve;
289
  let reject;
290
  const promise = new Promise<T>((_resolve, _reject) => {
163✔
291
    resolve = _resolve;
163✔
292
    reject = _reject;
163✔
293
  });
294
  // @ts-ignore Assigned in callback.
295
  return {promise, resolve, reject};
163✔
296
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc