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
// 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
# 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.tsBenefits
- Edge Case Discovery: Automatically finds corner cases that manual tests miss
- Regression Prevention: Catches bugs introduced by refactoring
- Documentation: Properties serve as executable specifications
- Confidence: Hundreds of test cases per property provide high coverage
- 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
- fast-check Documentation
- Property-Based Testing Guide
- Test files:
apps/web-client/src/reducers/mindMapReducer.test.tsapps/web-client/src/lib/coordinateTransforms.test.ts