@deck.gl/test-utils 9.3.0-alpha.3 → 9.3.0-alpha.5

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.
@@ -0,0 +1,42 @@
1
+ // deck.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ // Type declarations for browser test driver functions injected by @probe.gl/test-utils
6
+
7
+ interface BrowserTestDriverDiffOptions {
8
+ goldenImage: string;
9
+ region?: {x: number; y: number; width: number; height: number};
10
+ saveOnFail?: boolean;
11
+ saveAs?: string;
12
+ threshold?: number;
13
+ createDiffImage?: boolean;
14
+ tolerance?: number;
15
+ includeAA?: boolean;
16
+ includeEmpty?: boolean;
17
+ platform?: string;
18
+ }
19
+
20
+ interface BrowserTestDriverDiffResult {
21
+ headless: boolean;
22
+ match: string | number;
23
+ matchPercentage: string;
24
+ success: boolean;
25
+ error: Error | string | null;
26
+ }
27
+
28
+ interface BrowserTestDriverInputEvent {
29
+ type: string;
30
+ [key: string]: any;
31
+ }
32
+
33
+ declare global {
34
+ interface Window {
35
+ browserTestDriver_emulateInput(event: BrowserTestDriverInputEvent): Promise<void>;
36
+ browserTestDriver_captureAndDiffScreen(
37
+ options: BrowserTestDriverDiffOptions
38
+ ): Promise<BrowserTestDriverDiffResult>;
39
+ }
40
+ }
41
+
42
+ export {};
package/src/index.ts CHANGED
@@ -7,12 +7,8 @@ export {toLowPrecision} from './utils/precision';
7
7
  export {gl, device} from './utils/setup-gl';
8
8
 
9
9
  // Utilities for update tests (lifecycle tests)
10
- export {
11
- testLayer,
12
- testLayerAsync,
13
- testInitializeLayer,
14
- testInitializeLayerAsync
15
- } from './lifecycle-test';
10
+ // Re-export from tape.ts which provides default spy factory for backward compat
11
+ export {testLayer, testLayerAsync, testInitializeLayer, testInitializeLayerAsync} from './tape';
16
12
  export {generateLayerTests} from './generate-layer-tests';
17
13
 
18
14
  // Basic utility for rendering multiple scenes (could go into "deck.gl/core")
@@ -23,6 +19,6 @@ export {SnapshotTestRunner} from './snapshot-test-runner';
23
19
  // A utility that emulates input events
24
20
  export {InteractionTestRunner} from './interaction-test-runner';
25
21
 
26
- export type {LayerTestCase} from './lifecycle-test';
22
+ export type {LayerTestCase, ResetSpy, SpyFactory} from './tape';
27
23
  export type {SnapshotTestCase} from './snapshot-test-runner';
28
24
  export type {InteractionTestCase} from './interaction-test-runner';
@@ -4,7 +4,6 @@
4
4
 
5
5
  import {LayerManager, MapView, DeckRenderer} from '@deck.gl/core';
6
6
 
7
- import {makeSpy} from '@probe.gl/test-utils';
8
7
  import {device} from './utils/setup-gl';
9
8
 
10
9
  import type {Layer, CompositeLayer, Viewport} from '@deck.gl/core';
@@ -128,8 +127,25 @@ export async function testInitializeLayerAsync(
128
127
  return null;
129
128
  }
130
129
 
131
- // TODO - export from probe.gl
132
- type Spy = ReturnType<typeof makeSpy>;
130
+ /** Spy object compatible with both vitest and probe.gl */
131
+ export type Spy = {
132
+ /** Restore the original method (vitest) */
133
+ mockRestore?: () => void;
134
+ /** Restore the original method (probe.gl) */
135
+ restore?: () => void;
136
+ /** Call history (vitest) */
137
+ mock?: {calls: unknown[][]};
138
+ /** Call history (probe.gl) */
139
+ calls?: unknown[][];
140
+ /** Whether the spy was called (probe.gl) */
141
+ called?: boolean;
142
+ };
143
+
144
+ /** Factory function to create a spy on an object method */
145
+ export type SpyFactory = (obj: object, method: string) => Spy;
146
+
147
+ /** Function to reset/cleanup a spy after each test case */
148
+ export type ResetSpy = (spy: Spy) => void;
133
149
 
134
150
  export type LayerClass<LayerT extends Layer> = {
135
151
  new (...args): LayerT;
@@ -167,11 +183,7 @@ type TestResources = {
167
183
  oldResourceCounts: Record<string, number>;
168
184
  };
169
185
 
170
- /**
171
- * Initialize and updates a layer over a sequence of scenarios (test cases).
172
- * Use `testLayerAsync` if the layer's update flow contains async operations.
173
- */
174
- export function testLayer<LayerT extends Layer>(opts: {
186
+ export type TestLayerOptions<LayerT extends Layer> = {
175
187
  /** The layer class to test against */
176
188
  Layer: LayerClass<LayerT>;
177
189
  /** The initial viewport
@@ -189,8 +201,18 @@ export function testLayer<LayerT extends Layer>(opts: {
189
201
  spies?: string[];
190
202
  /** Callback if any error is thrown */
191
203
  onError?: (error: Error, title: string) => void;
192
- }): void {
193
- const {Layer, testCases = [], spies = [], onError = defaultOnError} = opts;
204
+ /** Factory function to create spies */
205
+ createSpy: SpyFactory;
206
+ /** Function to reset/cleanup a spy after each test case */
207
+ resetSpy: ResetSpy;
208
+ };
209
+
210
+ /**
211
+ * Initialize and updates a layer over a sequence of scenarios (test cases).
212
+ * Use `testLayerAsync` if the layer's update flow contains async operations.
213
+ */
214
+ export function testLayer<LayerT extends Layer>(opts: TestLayerOptions<LayerT>): void {
215
+ const {Layer, testCases = [], spies = [], onError = defaultOnError, createSpy, resetSpy} = opts;
194
216
 
195
217
  const resources = setupLayerTests(`testing ${Layer.layerName}`, opts);
196
218
 
@@ -200,12 +222,18 @@ export function testLayer<LayerT extends Layer>(opts: {
200
222
  // Save old state before update
201
223
  const oldState = {...layer.state};
202
224
 
203
- const {layer: newLayer, spyMap} = runLayerTestUpdate(testCase, resources, layer, spies);
225
+ const {layer: newLayer, spyMap} = runLayerTestUpdate(
226
+ testCase,
227
+ resources,
228
+ layer,
229
+ spies,
230
+ createSpy
231
+ );
204
232
 
205
233
  runLayerTestPostUpdateCheck(testCase, newLayer, oldState, spyMap);
206
234
 
207
- // Remove spies
208
- Object.keys(spyMap).forEach(k => spyMap[k].reset());
235
+ // Reset spies between test cases
236
+ Object.keys(spyMap).forEach(k => resetSpy(spyMap[k]));
209
237
  layer = newLayer;
210
238
  }
211
239
 
@@ -219,26 +247,10 @@ export function testLayer<LayerT extends Layer>(opts: {
219
247
  * Initialize and updates a layer over a sequence of scenarios (test cases).
220
248
  * Each test case is awaited until the layer's isLoaded flag is true.
221
249
  */
222
- export async function testLayerAsync<LayerT extends Layer>(opts: {
223
- /** The layer class to test against */
224
- Layer: LayerClass<LayerT>;
225
- /** The initial viewport
226
- * @default WebMercatorViewport
227
- */
228
- viewport?: Viewport;
229
- /**
230
- * If provided, used to controls time progression. Useful for testing transitions and animations.
231
- */
232
- timeline?: Timeline;
233
- testCases?: LayerTestCase<LayerT>[];
234
- /**
235
- * List of layer method names to watch
236
- */
237
- spies?: string[];
238
- /** Callback if any error is thrown */
239
- onError?: (error: Error, title: string) => void;
240
- }): Promise<void> {
241
- const {Layer, testCases = [], spies = [], onError = defaultOnError} = opts;
250
+ export async function testLayerAsync<LayerT extends Layer>(
251
+ opts: TestLayerOptions<LayerT>
252
+ ): Promise<void> {
253
+ const {Layer, testCases = [], spies = [], onError = defaultOnError, createSpy, resetSpy} = opts;
242
254
 
243
255
  const resources = setupLayerTests(`testing ${Layer.layerName}`, opts);
244
256
 
@@ -248,7 +260,13 @@ export async function testLayerAsync<LayerT extends Layer>(opts: {
248
260
  // Save old state before update
249
261
  const oldState = {...layer.state};
250
262
 
251
- const {layer: newLayer, spyMap} = runLayerTestUpdate(testCase, resources, layer, spies);
263
+ const {layer: newLayer, spyMap} = runLayerTestUpdate(
264
+ testCase,
265
+ resources,
266
+ layer,
267
+ spies,
268
+ createSpy
269
+ );
252
270
 
253
271
  runLayerTestPostUpdateCheck(testCase, newLayer, oldState, spyMap);
254
272
 
@@ -257,12 +275,13 @@ export async function testLayerAsync<LayerT extends Layer>(opts: {
257
275
  runLayerTestPostUpdateCheck(testCase, newLayer, oldState, spyMap);
258
276
  }
259
277
 
260
- // Remove spies
261
- Object.keys(spyMap).forEach(k => spyMap[k].reset());
278
+ // Reset spies between test cases
279
+ Object.keys(spyMap).forEach(k => resetSpy(spyMap[k]));
262
280
  layer = newLayer;
263
281
  }
264
282
 
265
- const error = cleanupAfterLayerTests(resources);
283
+ // Use async cleanup to allow pending luma.gl async operations to complete
284
+ const error = await cleanupAfterLayerTestsAsync(resources);
266
285
  if (error) {
267
286
  onError(error, `${Layer.layerName} should delete all resources`);
268
287
  }
@@ -305,16 +324,31 @@ function cleanupAfterLayerTests({
305
324
  layerManager.finalize();
306
325
  deckRenderer.finalize();
307
326
 
308
- const resourceCounts = getResourceCounts();
327
+ return getResourceCountDelta(oldResourceCounts);
328
+ }
309
329
 
310
- for (const resourceName in resourceCounts) {
311
- if (resourceCounts[resourceName] !== oldResourceCounts[resourceName]) {
312
- return new Error(
313
- `${resourceCounts[resourceName] - oldResourceCounts[resourceName]} ${resourceName}s`
314
- );
315
- }
316
- }
317
- return null;
330
+ /**
331
+ * Async cleanup that waits for pending async operations before finalizing resources.
332
+ * This prevents unhandled rejections from luma.gl's async shader error reporting
333
+ * which may try to access destroyed WebGL resources if cleanup happens too early.
334
+ */
335
+ async function cleanupAfterLayerTestsAsync({
336
+ layerManager,
337
+ deckRenderer,
338
+ oldResourceCounts
339
+ }: TestResources): Promise<Error | null> {
340
+ layerManager.setLayers([]);
341
+
342
+ // Wait for any pending async operations (e.g., luma.gl's deferred shader compilation
343
+ // error handling) to complete before destroying resources. This prevents
344
+ // "getProgramInfoLog" errors when async error reporting tries to access
345
+ // already-destroyed WebGL programs.
346
+ await new Promise(resolve => setTimeout(resolve, 0));
347
+
348
+ layerManager.finalize();
349
+ deckRenderer.finalize();
350
+
351
+ return getResourceCountDelta(oldResourceCounts);
318
352
  }
319
353
 
320
354
  function getResourceCounts(): Record<string, number> {
@@ -326,11 +360,24 @@ function getResourceCounts(): Record<string, number> {
326
360
  };
327
361
  }
328
362
 
329
- function injectSpies(layer: Layer, spies: string[]): Record<string, Spy> {
363
+ function getResourceCountDelta(oldResourceCounts: Record<string, number>): Error | null {
364
+ const resourceCounts = getResourceCounts();
365
+
366
+ for (const resourceName in resourceCounts) {
367
+ if (resourceCounts[resourceName] !== oldResourceCounts[resourceName]) {
368
+ return new Error(
369
+ `${resourceCounts[resourceName] - oldResourceCounts[resourceName]} ${resourceName}s`
370
+ );
371
+ }
372
+ }
373
+ return null;
374
+ }
375
+
376
+ function injectSpies(layer: Layer, spies: string[], spyFactory: SpyFactory): Record<string, Spy> {
330
377
  const spyMap: Record<string, Spy> = {};
331
378
  if (spies) {
332
379
  for (const functionName of spies) {
333
- spyMap[functionName] = makeSpy(Object.getPrototypeOf(layer), functionName);
380
+ spyMap[functionName] = spyFactory(Object.getPrototypeOf(layer), functionName);
334
381
  }
335
382
  }
336
383
  return spyMap;
@@ -366,7 +413,8 @@ function runLayerTestUpdate<LayerT extends Layer>(
366
413
  testCase: LayerTestCase<LayerT>,
367
414
  {layerManager, deckRenderer}: TestResources,
368
415
  layer: LayerT,
369
- spies: string[]
416
+ spies: string[],
417
+ spyFactory: SpyFactory
370
418
  ): {
371
419
  layer: LayerT;
372
420
  spyMap: Record<string, Spy>;
@@ -387,7 +435,7 @@ function runLayerTestUpdate<LayerT extends Layer>(
387
435
 
388
436
  // Create a map of spies that the test case can inspect
389
437
  spies = testCase.spies || spies;
390
- const spyMap = injectSpies(layer, spies);
438
+ const spyMap = injectSpies(layer, spies, spyFactory);
391
439
  const drawLayers = () => {
392
440
  deckRenderer.renderLayers({
393
441
  pass: 'test',
package/src/tape.ts ADDED
@@ -0,0 +1,85 @@
1
+ // deck.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ // Tape entry point - wraps lifecycle-test and adds @probe.gl/test-utils as the default spy factory
6
+ // For vitest users, use @deck.gl/test-utils/vitest which doesn't import probe.gl
7
+
8
+ import {makeSpy} from '@probe.gl/test-utils';
9
+ import {
10
+ testLayer as testLayerCore,
11
+ testLayerAsync as testLayerAsyncCore,
12
+ testInitializeLayer,
13
+ testInitializeLayerAsync
14
+ } from './lifecycle-test';
15
+ import type {Layer} from '@deck.gl/core';
16
+ import type {
17
+ LayerClass,
18
+ LayerTestCase,
19
+ ResetSpy,
20
+ SpyFactory,
21
+ TestLayerOptions
22
+ } from './lifecycle-test';
23
+
24
+ export {testInitializeLayer, testInitializeLayerAsync};
25
+ export type {LayerClass, LayerTestCase, ResetSpy, SpyFactory};
26
+
27
+ let _hasWarnedCreateSpy = false;
28
+ let _hasWarnedResetSpy = false;
29
+
30
+ function getDefaultSpyFactory(): SpyFactory {
31
+ if (!_hasWarnedCreateSpy) {
32
+ _hasWarnedCreateSpy = true;
33
+ // eslint-disable-next-line no-console
34
+ console.warn(
35
+ '[@deck.gl/test-utils] Implicit @probe.gl/test-utils usage is deprecated. ' +
36
+ 'Pass createSpy option: createSpy: (obj, method) => vi.spyOn(obj, method) for vitest, ' +
37
+ 'or createSpy: makeSpy for probe.gl.'
38
+ );
39
+ }
40
+ return makeSpy;
41
+ }
42
+
43
+ /** Default reset for probe.gl spies - clears call tracking but keeps spy active */
44
+ function getDefaultResetSpy(): ResetSpy {
45
+ if (!_hasWarnedResetSpy) {
46
+ _hasWarnedResetSpy = true;
47
+ // eslint-disable-next-line no-console
48
+ console.warn(
49
+ '[@deck.gl/test-utils] Implicit spy reset is deprecated. ' +
50
+ 'Pass resetSpy option: resetSpy: (spy) => spy.mockRestore() for vitest, ' +
51
+ 'or resetSpy: (spy) => spy.reset() for probe.gl.'
52
+ );
53
+ }
54
+ return spy => (spy as ReturnType<typeof makeSpy>).reset();
55
+ }
56
+
57
+ /**
58
+ * Initialize and updates a layer over a sequence of scenarios (test cases).
59
+ * Use `testLayerAsync` if the layer's update flow contains async operations.
60
+ */
61
+ export function testLayer<LayerT extends Layer>(
62
+ opts: Omit<TestLayerOptions<LayerT>, 'createSpy' | 'resetSpy'> & {
63
+ createSpy?: SpyFactory;
64
+ resetSpy?: ResetSpy;
65
+ }
66
+ ): void {
67
+ const createSpy = opts.createSpy || getDefaultSpyFactory();
68
+ const resetSpy = opts.resetSpy || getDefaultResetSpy();
69
+ testLayerCore({...opts, createSpy, resetSpy});
70
+ }
71
+
72
+ /**
73
+ * Initialize and updates a layer over a sequence of scenarios (test cases).
74
+ * Each test case is awaited until the layer's isLoaded flag is true.
75
+ */
76
+ export async function testLayerAsync<LayerT extends Layer>(
77
+ opts: Omit<TestLayerOptions<LayerT>, 'createSpy' | 'resetSpy'> & {
78
+ createSpy?: SpyFactory;
79
+ resetSpy?: ResetSpy;
80
+ }
81
+ ): Promise<void> {
82
+ const createSpy = opts.createSpy || getDefaultSpyFactory();
83
+ const resetSpy = opts.resetSpy || getDefaultResetSpy();
84
+ await testLayerAsyncCore({...opts, createSpy, resetSpy});
85
+ }
@@ -163,7 +163,7 @@ export abstract class TestRunner<TestCaseT extends TestCase, ResultT, ExtraOptio
163
163
  const task = this.runTestCase(testCase);
164
164
  const timeoutTask = new Promise((_, reject) => {
165
165
  setTimeout(() => {
166
- reject('Timeout');
166
+ reject(`Timeout after ${timeout}ms`);
167
167
  }, timeout);
168
168
  });
169
169
 
@@ -171,8 +171,8 @@ export abstract class TestRunner<TestCaseT extends TestCase, ResultT, ExtraOptio
171
171
  await Promise.race([task, timeoutTask]);
172
172
  await this.assert(testCase);
173
173
  } catch (err: unknown) {
174
- if (err === 'Timeout') {
175
- this.fail({error: 'Timeout'});
174
+ if (typeof err === 'string' && err.startsWith('Timeout')) {
175
+ this.fail({error: err});
176
176
  }
177
177
  }
178
178
  }
@@ -2,10 +2,9 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import {CanvasContextProps} from '@luma.gl/core';
6
- import {WebGLDevice} from '@luma.gl/webgl';
7
5
  import {webglDevice, NullDevice} from '@luma.gl/test-utils';
8
6
 
7
+ // Use pre-created device from @luma.gl/test-utils, fall back to NullDevice in Node
9
8
  export const device = webglDevice || new NullDevice({});
10
9
  export const gl = webglDevice?.gl || 1;
11
10
 
package/src/vitest.ts ADDED
@@ -0,0 +1,49 @@
1
+ // deck.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ // Vitest-specific entry point with vi.spyOn default
6
+ // Use: import { testLayer } from '@deck.gl/test-utils/vitest'
7
+
8
+ import {vi} from 'vitest';
9
+ import {testLayer as testLayerCore, testLayerAsync as testLayerAsyncCore} from './lifecycle-test';
10
+ import type {Layer} from '@deck.gl/core';
11
+ import type {ResetSpy, SpyFactory, TestLayerOptions} from './lifecycle-test';
12
+
13
+ /** Default spy factory using vi.spyOn */
14
+ const defaultSpyFactory: SpyFactory = (obj, method) => vi.spyOn(obj, method as never);
15
+
16
+ /** Default reset for vitest spies - restores original implementation */
17
+ const defaultResetSpy: ResetSpy = spy => spy.mockRestore?.();
18
+
19
+ export function testLayer<LayerT extends Layer>(
20
+ opts: Omit<TestLayerOptions<LayerT>, 'createSpy' | 'resetSpy'> & {
21
+ createSpy?: SpyFactory;
22
+ resetSpy?: ResetSpy;
23
+ }
24
+ ) {
25
+ const createSpy = opts.createSpy || defaultSpyFactory;
26
+ const resetSpy = opts.resetSpy || defaultResetSpy;
27
+ return testLayerCore({...opts, createSpy, resetSpy});
28
+ }
29
+
30
+ export function testLayerAsync<LayerT extends Layer>(
31
+ opts: Omit<TestLayerOptions<LayerT>, 'createSpy' | 'resetSpy'> & {
32
+ createSpy?: SpyFactory;
33
+ resetSpy?: ResetSpy;
34
+ }
35
+ ) {
36
+ const createSpy = opts.createSpy || defaultSpyFactory;
37
+ const resetSpy = opts.resetSpy || defaultResetSpy;
38
+ return testLayerAsyncCore({...opts, createSpy, resetSpy});
39
+ }
40
+
41
+ // Re-export non-spy utilities
42
+ export {testInitializeLayer, testInitializeLayerAsync} from './lifecycle-test';
43
+ export {getLayerUniforms} from './utils/layer';
44
+ export {toLowPrecision} from './utils/precision';
45
+ export {gl, device} from './utils/setup-gl';
46
+ export {generateLayerTests} from './generate-layer-tests';
47
+
48
+ // Types
49
+ export type {LayerTestCase, ResetSpy, SpyFactory, TestLayerOptions} from './lifecycle-test';