Event simulation and interaction flow testing for deck.gl applications. Emulates user input events like mouse clicks, drags, and keyboard input to validate application state changes and interactive behaviors.
Primary class for managing interaction test execution.
/**
* Test runner that emulates input events for interaction testing
* Extends the base TestRunner for event simulation and validation
*/
class InteractionTestRunner extends TestRunner<InteractionTestCase, {}> {
/**
* Create a new interaction test runner
* @param props - Deck.gl props for application setup
* @param options - Test runner options
*/
constructor(props: DeckProps, options?: {});
/**
* Add test cases to the runner
* @param testCases - Array of interaction test cases
* @returns This runner for method chaining
*/
add(testCases: InteractionTestCase[]): this;
/**
* Execute all test cases
* @param options - Runtime options for test execution
* @returns Promise that resolves when all tests complete
*/
run(options?: Partial<TestOptions<InteractionTestCase, {}>>): Promise<void>;
}Structure for defining individual interaction test scenarios.
interface InteractionTestCase {
/** Descriptive name for the test case */
name: string;
/** Sequence of events to simulate */
events: InteractionEvent[];
/** Maximum time to wait for this test case (milliseconds) */
timeout?: number;
/** Context data shared between callbacks */
context?: any;
/** Called before event simulation begins */
onBeforeEvents: (params: { deck: Deck }) => any;
/** Called after all events are simulated */
onAfterEvents: (params: { deck: Deck; layers: Layer[]; context: any }) => void;
}Events that can be simulated in interaction tests.
type InteractionEvent =
| {
/** Event type (e.g., 'click', 'mousemove', 'keydown') */
type: string;
/** Additional event properties (coordinates, key codes, etc.) */
[key: string]: any;
}
| {
/** Wait duration in milliseconds */
wait: number;
};Usage Examples:
import { InteractionTestRunner } from "@deck.gl/test-utils";
import { ScatterplotLayer, Deck } from "@deck.gl/layers";
// Create test runner
const testRunner = new InteractionTestRunner({
width: 800,
height: 600,
views: [new MapView()],
initialViewState: {
longitude: -122.4,
latitude: 37.8,
zoom: 12
},
controller: true
});
// Add interaction test cases
testRunner.add([
{
name: "Click on scatterplot point",
events: [
{ type: 'click', x: 400, y: 300, button: 0 }
],
onBeforeEvents: ({ deck }) => {
// Set up layers with click handlers
deck.setProps({
layers: [
new ScatterplotLayer({
id: 'scatter',
data: [
{ position: [-122.4, 37.8], id: 'point1' },
{ position: [-122.41, 37.81], id: 'point2' }
],
getPosition: d => d.position,
pickable: true,
onClick: (info) => {
console.log('Clicked point:', info.object.id);
return { clickedPoint: info.object };
}
})
]
});
return { clickedPoint: null };
},
onAfterEvents: ({ deck, layers, context }) => {
// Validate that click was handled correctly
if (!context.clickedPoint) {
throw new Error('No point was clicked');
}
console.log('Successfully clicked:', context.clickedPoint.id);
}
},
{
name: "Drag to pan map",
events: [
{ type: 'mousedown', x: 400, y: 300, button: 0 },
{ wait: 100 },
{ type: 'mousemove', x: 500, y: 300 },
{ wait: 100 },
{ type: 'mousemove', x: 600, y: 300 },
{ wait: 100 },
{ type: 'mouseup', x: 600, y: 300, button: 0 }
],
onBeforeEvents: ({ deck }) => {
const initialViewState = deck.props.initialViewState;
return {
initialLongitude: initialViewState.longitude,
initialLatitude: initialViewState.latitude
};
},
onAfterEvents: ({ deck, context }) => {
const currentViewState = deck.props.viewState || deck.props.initialViewState;
// Validate that view state changed due to panning
if (currentViewState.longitude === context.initialLongitude) {
throw new Error('Map did not pan - longitude unchanged');
}
console.log('Pan successful:', {
from: context.initialLongitude,
to: currentViewState.longitude
});
}
},
{
name: "Keyboard navigation",
events: [
{ type: 'keydown', key: 'ArrowUp' },
{ wait: 200 },
{ type: 'keyup', key: 'ArrowUp' },
{ wait: 100 },
{ type: 'keydown', key: 'ArrowRight' },
{ wait: 200 },
{ type: 'keyup', key: 'ArrowRight' }
],
timeout: 5000,
onBeforeEvents: ({ deck }) => {
// Enable keyboard controls
deck.setProps({
controller: {
keyboard: true
}
});
return {
initialViewState: { ...deck.props.initialViewState }
};
},
onAfterEvents: ({ deck, context }) => {
const currentViewState = deck.props.viewState || deck.props.initialViewState;
// Validate keyboard navigation worked
const latChanged = currentViewState.latitude !== context.initialViewState.latitude;
const lngChanged = currentViewState.longitude !== context.initialViewState.longitude;
if (!latChanged && !lngChanged) {
throw new Error('Keyboard navigation had no effect');
}
console.log('Keyboard navigation successful');
}
}
]);
// Run tests with custom callbacks
await testRunner.run({
timeout: 15000,
onTestStart: testCase => console.log(`🖱️ ${testCase.name}`),
onTestPass: testCase => console.log(`✓ ${testCase.name} completed`),
onTestFail: (testCase, result) => console.error(`✗ ${testCase.name} failed:`, result.error)
});Common mouse event patterns:
// Click
{ type: 'click', x: 400, y: 300, button: 0 }
// Drag sequence
{ type: 'mousedown', x: 100, y: 100, button: 0 },
{ type: 'mousemove', x: 200, y: 100 },
{ type: 'mouseup', x: 200, y: 100, button: 0 }
// Hover
{ type: 'mousemove', x: 300, y: 200 }
// Right click
{ type: 'click', x: 400, y: 300, button: 2 }Common keyboard event patterns:
// Key press and release
{ type: 'keydown', key: 'Enter' },
{ type: 'keyup', key: 'Enter' }
// Modified key combinations
{ type: 'keydown', key: 'z', ctrlKey: true },
{ type: 'keyup', key: 'z', ctrlKey: true }
// Arrow keys for navigation
{ type: 'keydown', key: 'ArrowLeft' },
{ type: 'keyup', key: 'ArrowLeft' }Touch event patterns for mobile testing:
// Tap
{ type: 'touchstart', touches: [{ x: 400, y: 300 }] },
{ type: 'touchend', touches: [] }
// Pinch zoom
{ type: 'touchstart', touches: [{ x: 300, y: 300 }, { x: 500, y: 300 }] },
{ type: 'touchmove', touches: [{ x: 250, y: 300 }, { x: 550, y: 300 }] },
{ type: 'touchend', touches: [] }Timing control between events:
// Wait for animations or state changes
{ wait: 500 } // Wait 500ms
// Common patterns
{ type: 'mousedown', x: 100, y: 100 },
{ wait: 100 }, // Brief pause for mousedown to register
{ type: 'mousemove', x: 200, y: 100 },
{ wait: 50 }, // Animation frame delay
{ type: 'mouseup', x: 200, y: 100 }Called once before event simulation begins. Use for:
Must return context object that will be passed to onAfterEvents.
Called after all events are simulated. Use for:
Receives the context object returned from onBeforeEvents.
{
name: "Hover effects",
events: [
{ type: 'mousemove', x: 300, y: 200 } // Hover over feature
],
onBeforeEvents: ({ deck }) => {
deck.setProps({
layers: [
new GeoJsonLayer({
data: featureCollection,
pickable: true,
onHover: (info) => {
// Track hover state
window.testHoverInfo = info;
}
})
]
});
return {};
},
onAfterEvents: () => {
if (!window.testHoverInfo?.object) {
throw new Error('Hover event not triggered');
}
}
}{
name: "Zoom with mouse wheel",
events: [
{ type: 'wheel', x: 400, y: 300, deltaY: -120 } // Scroll up to zoom in
],
onBeforeEvents: ({ deck }) => ({
initialZoom: deck.props.initialViewState.zoom
}),
onAfterEvents: ({ deck, context }) => {
const currentZoom = (deck.props.viewState || deck.props.initialViewState).zoom;
if (currentZoom <= context.initialZoom) {
throw new Error('Zoom level did not increase');
}
}
}{
name: "Complex selection workflow",
events: [
{ type: 'click', x: 300, y: 200 }, // Select first item
{ wait: 100 },
{ type: 'click', x: 400, y: 250, ctrlKey: true }, // Multi-select second item
{ wait: 100 },
{ type: 'keydown', key: 'Delete' }, // Delete selected items
{ type: 'keyup', key: 'Delete' }
],
onBeforeEvents: ({ deck }) => {
// Set up selection state management
return { selectedItems: [] };
},
onAfterEvents: ({ context }) => {
// Validate multi-step interaction results
if (context.selectedItems.length > 0) {
throw new Error('Items were not deleted');
}
}
}