Skip to content

Property-Based Testing in Kemma

This document summarizes the property-based tests implemented for the Kemma web client using fast-check.

Overview

Property-based testing automatically generates hundreds of random test cases to verify that code satisfies mathematical properties and invariants. Unlike example-based tests that check specific inputs, property-based tests catch edge cases across the entire input space.

Test Coverage

1. Mind Map Reducer (src/reducers/mindMapReducer.test.ts)

8 tests covering state management logic for the mind map editor.

Tree Structure Invariants (4 tests)

  • ADD_NODE maintains bidirectional parent-child references

    • Property: Parent's children array matches actual children
    • Property: All children reference correct parent
  • DELETE_NODES removes all descendants

    • Property: Deleting a node removes entire subtree
    • Property: Only root node remains after deleting first-level child
  • DELETE_NODES never creates orphaned nodes

    • Property: All remaining nodes have valid parent references
    • Property: No dangling parentId pointers
  • root node cannot be deleted

    • Property: Root always exists after delete operation

History Management (2 tests)

  • UNDO followed by REDO returns to original state

    • Property: Node count matches after undo/redo cycle
    • Property: State is reversible
  • history never contains invalid states

    • Property: All history entries contain root node
    • Property: History stack maintains valid snapshots

Collapse Operations (1 test)

  • TOGGLE_COLLAPSE propagates visibility to descendants
    • Property: Collapsing node hides all descendants
    • Property: Expanding node shows descendants again

Selection Operations (1 test)

  • SELECT_DESCENDANTS includes all descendants
    • Property: All descendants are selected
    • Property: Siblings outside subtree are not selected

2. Coordinate Transforms (src/lib/coordinateTransforms.test.ts)

12 tests covering canvas coordinate mathematics for zoom, pan, and drag operations.

Drag Operations (2 tests)

  • drag offset + node position calculation is reversible

    • Property: Calculating offset then position returns original node position
    • Property: Operations are inverse of each other
  • dragging with offset keeps relative position stable

    • Property: Delta in node position equals delta in mouse position (scaled by zoom)
    • Property: Relative positioning is consistent

Zoom Operations (5 tests)

  • zoom in then zoom out returns to original offset

    • Property: Zoom operations are reversible
    • Property: View offset returns to starting value
  • zoom keeps mouse position stable in canvas coordinates

    • Property: Mouse stays over same canvas point during zoom
    • Property: Zoom pivot point is maintained
  • clampZoom always returns value in valid range

    • Property: Result is always between 0.1 and 5.0
    • Property: Invalid inputs are clamped correctly
  • clampZoom with custom bounds respects those bounds

    • Property: Result respects min/max parameters
    • Property: Works with arbitrary bounds
  • drag offset maintains consistency across zoom levels

    • Property: Drag calculations work at any zoom level
    • Property: Both zoom levels return to original node position

Pan Operations (1 test)

  • pan offset calculation is consistent
    • Property: Offset places mouse at pan start in canvas coordinates
    • Property: Pan math is correct

Coordinate Conversions (3 tests)

  • screenToCanvas and canvasToScreen are inverse operations

    • Property: Screen → Canvas → Screen returns original point
    • Property: Bidirectional conversion is lossless
  • canvasToScreen and screenToCanvas are inverse operations

    • Property: Canvas → Screen → Canvas returns original point
    • Property: Reverse direction also works
  • zoom scales screen coordinates proportionally

    • Property: Doubling zoom doubles screen distance
    • Property: Scaling is linear

Combined Operations (1 test)

  • pan then zoom maintains relative positions
    • Property: Operations compose without errors
    • Property: Results are finite and defined

Implementation Details

Library

  • fast-check 4.3.0 - Property-based testing framework for TypeScript/JavaScript

Arbitrary Generators

typescript
// Points with coordinates in reasonable range
const arbPoint = fc.record({
  x: fc.float({ min: Math.fround(-10000), max: Math.fround(10000), noNaN: true }),
  y: fc.float({ min: Math.fround(-10000), max: Math.fround(10000), noNaN: true })
});

// Zoom levels matching application constraints
const arbZoom = fc.float({ min: Math.fround(0.1), max: Math.fround(5.0), noNaN: true });

Running Tests

bash
# Run all property-based tests
npx vitest run mindMapReducer.test.ts coordinateTransforms.test.ts

# Run with verbose output
npx vitest run --reporter=verbose

# Run specific test file
npx vitest run coordinateTransforms.test.ts

Benefits

  1. Edge Case Discovery: Automatically finds corner cases that manual tests miss
  2. Regression Prevention: Catches bugs introduced by refactoring
  3. Documentation: Properties serve as executable specifications
  4. Confidence: Hundreds of test cases per property provide high coverage
  5. Shrinking: fast-check automatically minimizes failing inputs for easier debugging

Future Opportunities

Additional areas identified for property-based testing:

Export Utilities (src/lib/exportUtils.ts)

  • Bounding box always contains all visible nodes
  • Filename generation produces valid filenames
  • Timestamps are monotonically increasing

Markdown Exporter (src/lib/MarkdownExporter.ts)

  • Text sanitization escapes all special characters
  • Tree traversal visits each node exactly once
  • Hierarchy preservation in output

Color Assignment

  • Root children get unique colors from palette
  • Descendants inherit parent color
  • Color index wraps correctly for >7 children

References