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

visgl / deck.gl / 23689953389

28 Mar 2026 05:00PM UTC coverage: 79.878% (-11.0%) from 90.907%
23689953389

push

github

web-flow
chore(test): vitest migration (#9969)

3050 of 3709 branches covered (82.23%)

Branch coverage included in aggregate %.

131 of 169 new or added lines in 11 files covered. (77.51%)

171 existing lines in 60 files now uncovered.

13988 of 17621 relevant lines covered (79.38%)

26917.67 hits per line

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

84.81
/test/render/deck-test-utils.ts
1
// deck.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import {expect} from 'vitest';
6
import {commands} from 'vitest/browser';
7
import {Deck, MapView} from '@deck.gl/core';
8
import {WIDTH, HEIGHT, OS} from './constants';
9

10
export interface TestCase {
11
  name: string;
12
  skip?: boolean;
13
  views?: any;
14
  viewState: any;
15
  layers: any[];
16
  effects?: any[];
17
  useDevicePixels?: boolean | number;
18
  onBeforeRender?: (params: {deck: Deck; layers: any[]}) => void;
19
  onAfterRender?: (params: {deck: Deck; layers: any[]; done: () => void}) => void;
20
  goldenImage: string;
21
  imageDiffOptions?: {
22
    threshold?: number;
23
    tolerance?: number;
24
  };
25
}
26

27
export interface DeckTestContext {
28
  deck: Deck | null;
29
  container: HTMLDivElement | null;
30
}
31

32
/**
33
 * Creates the container element for Deck tests.
34
 * Call this in beforeAll.
35
 */
36
export function createContainer(): HTMLDivElement {
37
  // Hide scrollbars to prevent them from appearing in screenshots
38
  document.body.style.cssText = 'margin: 0; padding: 0; overflow: hidden;';
29✔
39

40
  const container = document.createElement('div');
29✔
41
  container.id = 'deck-container';
29✔
42
  container.style.cssText = `position: absolute; left: 0; top: 0; width: ${WIDTH}px; height: ${HEIGHT}px;`;
29✔
43
  document.body.appendChild(container);
29✔
44
  return container;
29✔
45
}
46

47
/**
48
 * Removes the container element.
49
 * Call this in afterAll.
50
 */
51
export function removeContainer(container: HTMLDivElement | null): void {
52
  if (container) {
29✔
53
    container.remove();
29✔
54
  }
55
}
56

57
/**
58
 * Finalizes the deck instance.
59
 * Call this in afterEach.
60
 */
61
export function finalizeDeck(ctx: DeckTestContext): void {
62
  if (ctx.deck) {
171✔
63
    ctx.deck.finalize();
144✔
64
    ctx.deck = null;
144✔
65
  }
66
}
67

68
/**
69
 * Default render completion check - matches the old SnapshotTestRunner behavior.
70
 * Called after each render until layers are loaded and no more updates needed.
71
 */
72
export function defaultOnAfterRender({
73
  deck,
74
  layers,
75
  done
76
}: {
77
  deck: Deck;
78
  layers: any[];
79
  done: () => void;
80
}): void {
81
  // @ts-expect-error Accessing protected layerManager
82
  const needsUpdate = deck.layerManager?.needsUpdate();
445✔
83
  const allLoaded = layers.every((layer: any) => layer.isLoaded);
937✔
84

85
  if (!needsUpdate && allLoaded) {
445✔
86
    done();
327✔
87
  }
88
}
89

90
/**
91
 * Creates a Deck instance for reuse across multiple tests.
92
 * Use with updateDeckForTest() for tests that need the animation loop to keep running.
93
 */
94
export function createDeck(container: HTMLDivElement): Deck {
95
  return new Deck({
2✔
96
    id: 'render-test-deck',
97
    container,
98
    width: WIDTH,
99
    height: HEIGHT,
100
    useDevicePixels: false,
101
    debug: true
102
  });
103
}
104

105
/**
106
 * Helper to get canvas bounding box for screenshot region
107
 */
108
function getCanvasRegion(deck: Deck | null) {
109
  const canvas = deck?.getCanvas();
151✔
110
  if (!canvas) {
151✔
NEW
111
    return {x: 0, y: 0, width: WIDTH, height: HEIGHT};
×
112
  }
113
  const rect = canvas.getBoundingClientRect();
151✔
114
  return {
151✔
115
    x: Math.round(window.scrollX + rect.left),
116
    y: Math.round(window.scrollY + rect.top),
117
    width: Math.round(rect.width),
118
    height: Math.round(rect.height)
119
  };
120
}
121

122
/**
123
 * Runs a single render test case.
124
 * Creates a Deck instance, waits for render completion, captures and diffs screenshot.
125
 */
126
export async function runRenderTest(
127
  testCase: TestCase,
128
  ctx: DeckTestContext,
129
  timeout = 60000
130
): Promise<void> {
131
  const {views, viewState, layers, effects, useDevicePixels, onBeforeRender, onAfterRender} =
132
    testCase;
142✔
133

134
  // Create a new Deck instance for each test (like the old SnapshotTestRunner)
135
  // This ensures Deck enters a fresh render loop and properly handles async loading
136
  await new Promise<void>((resolve, reject) => {
142✔
137
    const timeoutId = setTimeout(() => {
142✔
NEW
138
      reject(new Error('Timeout waiting for render to complete'));
×
139
    }, timeout);
140

141
    const onAfterRenderCheck = onAfterRender || defaultOnAfterRender;
142✔
142

143
    ctx.deck = new Deck({
142✔
144
      id: 'render-test-deck',
145
      container: ctx.container!,
146
      width: WIDTH,
147
      height: HEIGHT,
148
      views: views || new MapView({}),
149
      viewState,
150
      layers,
151
      effects: effects || [],
152
      useDevicePixels: useDevicePixels ?? false,
153
      debug: true,
154

155
      onLoad: () => {
156
        // Call onBeforeRender if provided
157
        if (onBeforeRender) {
142✔
NEW
158
          onBeforeRender({
×
159
            deck: ctx.deck!,
160
            // @ts-expect-error Accessing protected layerManager
161
            layers: ctx.deck!.layerManager?.getLayers() || []
162
          });
163
        }
164
      },
165

166
      onAfterRender: () => {
167
        // @ts-expect-error Accessing protected layerManager
168
        const currentLayers = ctx.deck!.layerManager?.getLayers() || [];
359✔
169

170
        // Skip if no layers yet (Deck still initializing)
171
        if (currentLayers.length === 0) {
359✔
NEW
172
          return;
×
173
        }
174

175
        onAfterRenderCheck({
359✔
176
          deck: ctx.deck!,
177
          layers: currentLayers,
178
          done: () => {
179
            clearTimeout(timeoutId);
244✔
180
            resolve();
244✔
181
          }
182
        });
183
      }
184
    });
185
  });
186

187
  // Capture and diff screenshot
188
  await captureAndDiffScreenshot(testCase, ctx);
142✔
189
}
190

191
/**
192
 * Captures and diffs a screenshot against a golden image.
193
 * Used by both runRenderTest and updateDeckForTest.
194
 */
195
async function captureAndDiffScreenshot(testCase: TestCase, ctx: DeckTestContext): Promise<void> {
196
  const {name, goldenImage, imageDiffOptions} = testCase;
151✔
197

198
  const region = getCanvasRegion(ctx.deck);
151✔
199
  const diffOptions = {
151✔
200
    goldenImage,
201
    region,
202
    threshold: imageDiffOptions?.threshold ?? 0.99,
203
    tolerance: 0.1,
204
    includeEmpty: false,
205
    platform: OS,
206
    saveOnFail: true,
207
    createDiffImage: true
208
  };
209

210
  const result = await commands.captureAndDiffScreen(diffOptions);
151✔
211

212
  // If failed, try platform-specific golden image
213
  let finalResult = result;
151✔
214
  if (!result.success) {
151✔
NEW
215
    const platformGoldenImage = goldenImage.replace(
×
216
      'golden-images/',
217
      `golden-images/platform-overrides/${OS.toLowerCase()}/`
218
    );
NEW
219
    const platformResult = await commands.captureAndDiffScreen({
×
220
      ...diffOptions,
221
      goldenImage: platformGoldenImage
222
    });
NEW
223
    if (platformResult.success) {
×
NEW
224
      finalResult = platformResult;
×
225
    }
226
  }
227

228
  expect(
151✔
229
    finalResult.success,
230
    `${name}: ${finalResult.error || `match: ${finalResult.matchPercentage}%`}`
231
  ).toBe(true);
232
}
233

234
/**
235
 * Updates an existing Deck instance for a test case using setProps().
236
 * Use this instead of runRenderTest when tests need the animation loop to keep running
237
 * between onAfterRender callbacks (e.g., for timeline/transition tests).
238
 *
239
 * The Deck instance must be created beforehand with createDeck().
240
 */
241
export async function updateDeckForTest(
242
  testCase: TestCase,
243
  ctx: DeckTestContext,
244
  timeout = 60000
245
): Promise<void> {
246
  const {views, viewState, layers, effects, useDevicePixels, onBeforeRender, onAfterRender} =
247
    testCase;
9✔
248

249
  if (!ctx.deck) {
9✔
NEW
250
    throw new Error('Deck instance not found. Call createDeck() in beforeAll first.');
×
251
  }
252

253
  // Use setProps on existing deck - keeps the animation loop running
254
  await new Promise<void>((resolve, reject) => {
9✔
255
    const timeoutId = setTimeout(() => {
9✔
NEW
256
      reject(new Error('Timeout waiting for render to complete'));
×
257
    }, timeout);
258

259
    const onAfterRenderCheck = onAfterRender || defaultOnAfterRender;
9✔
260

261
    ctx.deck!.setProps({
9✔
262
      views: views || new MapView({}),
263
      viewState,
264
      layers,
265
      effects: effects || [],
266
      useDevicePixels: useDevicePixels ?? false,
267

268
      // onBeforeRender is called before each render frame - used for timeline setup
269
      // Always provide a function to clear any previous callback
270
      onBeforeRender: () => {
271
        if (onBeforeRender) {
130✔
272
          onBeforeRender({
89✔
273
            deck: ctx.deck!,
274
            // @ts-expect-error Accessing protected layerManager
275
            layers: ctx.deck!.layerManager?.getLayers() || []
276
          });
277
        }
278
      },
279

280
      onAfterRender: () => {
281
        // @ts-expect-error Accessing protected layerManager
282
        const currentLayers = ctx.deck!.layerManager?.getLayers() || [];
130✔
283

284
        // Skip if no layers yet (Deck still initializing)
285
        if (currentLayers.length === 0) {
130✔
NEW
286
          return;
×
287
        }
288

289
        onAfterRenderCheck({
130✔
290
          deck: ctx.deck!,
291
          layers: currentLayers,
292
          done: () => {
293
            clearTimeout(timeoutId);
124✔
294
            resolve();
124✔
295
          }
296
        });
297
      }
298
    });
299
  });
300

301
  // Capture and diff screenshot
302
  await captureAndDiffScreenshot(testCase, ctx);
9✔
303
}
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