or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

index.mdinteraction-testing.mdlifecycle-testing.mdtest-generation.mdtesting-utilities.mdvisual-testing.md
tile.json

interaction-testing.mddocs/

Interaction Testing

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.

Capabilities

Interaction Test Runner

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>;
}

Interaction Test Case Configuration

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;
}

Event Types

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)
});

Event Simulation

Mouse Events

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 }

Keyboard Events

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 Events

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: [] }

Wait Events

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 }

Test Lifecycle

onBeforeEvents

Called once before event simulation begins. Use for:

  • Setting up layer configurations
  • Enabling interaction handlers
  • Initializing context data
  • Recording initial state

Must return context object that will be passed to onAfterEvents.

onAfterEvents

Called after all events are simulated. Use for:

  • Validating state changes
  • Checking interaction results
  • Asserting expected behaviors
  • Cleanup operations

Receives the context object returned from onBeforeEvents.

Common Testing Patterns

Layer Interaction Testing

{
  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');
    }
  }
}

View State Testing

{
  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');
    }
  }
}

Multi-step Interactions

{
  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');
    }
  }
}