Screenshot-based testing that renders deck.gl scenes and compares against golden images for visual regression detection. Includes platform-specific overrides, diff image generation, and automated test orchestration.
Primary class for managing visual regression test execution.
/**
* Test runner that renders scenes and compares against golden images
* Extends the base TestRunner with image comparison capabilities
*/
class SnapshotTestRunner extends TestRunner<SnapshotTestCase, DiffImageResult, { imageDiffOptions: ImageDiffOptions }> {
/**
* Create a new snapshot test runner
* @param props - Deck.gl props for rendering configuration
* @param options - Image comparison options
*/
constructor(props: DeckProps, options?: { imageDiffOptions: ImageDiffOptions });
/**
* Add test cases to the runner
* @param testCases - Array of snapshot test cases
* @returns This runner for method chaining
*/
add(testCases: SnapshotTestCase[]): this;
/**
* Execute all test cases
* @param options - Runtime options for test execution
* @returns Promise that resolves when all tests complete
*/
run(options?: Partial<TestOptions<SnapshotTestCase, DiffImageResult> & { imageDiffOptions: ImageDiffOptions }>): Promise<void>;
}Structure for defining individual visual regression test cases.
interface SnapshotTestCase {
/** Descriptive name for the test case */
name: string;
/** Deck.gl props for rendering the scene */
props: DeckProps;
/** Path to the golden reference image */
goldenImage: string;
/** Called before rendering begins */
onBeforeRender?: (params: { deck: Deck; layers: Layer[] }) => void;
/** Called after each render frame - call done() when ready to capture */
onAfterRender?: (params: { deck: Deck; layers: Layer[]; done: () => void }) => void;
/** Maximum time to wait for this test case (milliseconds) */
timeout?: number;
/** Image comparison options specific to this test case */
imageDiffOptions?: ImageDiffOptions;
}Configuration for controlling image comparison behavior.
interface ImageDiffOptions {
/** Save failed comparison images to disk */
saveOnFail?: boolean;
/** Custom filename for saved images */
saveAs?: string;
/** Similarity threshold (0-1, where 1 is exact match) */
threshold?: number;
/** Generate diff image highlighting differences */
createDiffImage?: boolean;
/** Color tolerance for pixel comparison (0-1) */
tolerance?: number;
/** Include anti-aliasing in comparison */
includeAA?: boolean;
/** Include empty/transparent areas in comparison */
includeEmpty?: boolean;
/** Platform identifier for platform-specific golden images */
platform?: string;
}
interface DiffImageResult {
/** Whether running in headless browser mode */
headless: boolean;
/** Similarity score or match status */
match: string | number;
/** Human-readable match percentage */
matchPercentage: string;
/** Whether the comparison passed */
success: boolean;
/** Error information if comparison failed */
error: Error | string | null;
}Usage Examples:
import { SnapshotTestRunner } from "@deck.gl/test-utils";
import { ScatterplotLayer } from "@deck.gl/layers";
// Create test runner
const testRunner = new SnapshotTestRunner({
width: 800,
height: 600,
views: [new MapView()],
initialViewState: {
longitude: -122.4,
latitude: 37.8,
zoom: 12
}
}, {
imageDiffOptions: {
threshold: 0.99,
tolerance: 0.1,
saveOnFail: true
}
});
// Add test cases
testRunner.add([
{
name: "Scatterplot basic rendering",
props: {
layers: [
new ScatterplotLayer({
id: 'scatter',
data: [
{ position: [-122.4, 37.8], size: 100, color: [255, 0, 0] },
{ position: [-122.41, 37.81], size: 150, color: [0, 255, 0] }
],
getPosition: d => d.position,
getRadius: d => d.size,
getFillColor: d => d.color
})
]
},
goldenImage: './test/golden-images/scatterplot-basic.png',
onAfterRender: ({ deck, layers, done }) => {
// Wait for all layers to be loaded
if (layers.every(layer => layer.isLoaded)) {
done();
}
}
},
{
name: "Animated scatterplot",
props: {
layers: [
new ScatterplotLayer({
id: 'animated-scatter',
data: generateAnimatedData(),
getPosition: d => d.position,
getRadius: d => d.size,
transitions: {
getRadius: 1000
}
})
]
},
goldenImage: './test/golden-images/scatterplot-animated.png',
timeout: 5000,
onBeforeRender: ({ deck }) => {
console.log('Starting animation test');
},
onAfterRender: ({ deck, layers, done }) => {
// Wait for transitions to complete
const layer = layers[0];
if (layer.isLoaded && !layer.state.transitions?.getRadius) {
done();
}
}
}
]);
// Run tests
await testRunner.run({
timeout: 10000,
onTestStart: testCase => console.log(`Starting: ${testCase.name}`),
onTestPass: testCase => console.log(`✓ ${testCase.name}`),
onTestFail: (testCase, result) => console.error(`✗ ${testCase.name}:`, result.error)
});The test runner supports platform-specific golden images for handling rendering differences across operating systems and GPU vendors:
golden-images/ directorygolden-images/platform-overrides/{platform}/ directoryWhen a test fails against the primary golden image, the runner automatically attempts comparison against the platform-specific override if available.
// Platform detection is automatic
const testRunner = new SnapshotTestRunner({}, {
imageDiffOptions: {
platform: 'windows' // Detected automatically in browser
}
});
// Golden image paths:
// Primary: ./golden-images/my-test.png
// Override: ./golden-images/platform-overrides/windows/my-test.pngTest cases have fine-grained control over the render cycle:
Called once before rendering begins. Use for:
Called after each render frame with a done callback. Use for:
layer.isLoaded)done() when ready for screenshot{
name: "Complex scene with loading states",
props: { /* ... */ },
onAfterRender: ({ deck, layers, done }) => {
// Check multiple conditions
const allLoaded = layers.every(layer => layer.isLoaded);
const noTransitions = layers.every(layer =>
!layer.state.transitions ||
Object.keys(layer.state.transitions).length === 0
);
if (allLoaded && noTransitions) {
// Additional wait for GPU to finish rendering
setTimeout(done, 100);
}
// If conditions not met, onAfterRender will be called again next frame
}
}Failed tests provide detailed information:
Configure automatic saving of failed comparison images:
{
imageDiffOptions: {
saveOnFail: true,
saveAs: 'custom-failure-name.png',
createDiffImage: true // Also save highlighted diff
}
}Common patterns for debugging visual tests:
onBeforeRender to log layer configurationsonAfterRender to inspect layer states and loading statuscreateDiffImage to see exactly what pixels differresult.headless flag for browser mode differences