test(know-your-world): add comprehensive unit tests for interaction state machine
Add 45 unit tests covering all state transitions: - Initial state verification - IDLE state transitions (MOUSE_ENTER, SHOW_MAGNIFIER, TOUCH_START) - HOVERING state transitions (MOUSE_LEAVE, MOUSE_MOVE, MOUSE_DOWN, SHOW_MAGNIFIER) - MAGNIFIER_VISIBLE transitions (MOUSE_MOVE, DISMISS, REQUEST_PRECISION, EXPAND, TOUCH_START) - MAGNIFIER_PANNING transitions (TOUCH_MOVE, TOUCH_END, DISMISS_MAGNIFIER) - MAGNIFIER_PINCHING transitions (TOUCH_MOVE, TOUCH_END, ZOOM_THRESHOLD, DISMISS_MAGNIFIER) - MAGNIFIER_EXPANDED transitions (COLLAPSE, DISMISS) - MAP_PANNING_MOBILE transitions (TOUCH_MOVE, TOUCH_END, DISMISS) - MAP_PANNING_DESKTOP transitions (MOUSE_UP, MOUSE_LEAVE) - PRECISION_MODE transitions (MOUSE_MOVE, ESCAPE_BOUNDARY, EXIT_PRECISION) - RELEASING_PRECISION transitions (RELEASE_ANIMATION_DONE) - Context preservation across transitions - Previous state tracking - Invalid transitions (no-ops) Also adds missing DISMISS_MAGNIFIER handlers in MAGNIFIER_PANNING and MAGNIFIER_PINCHING states to ensure dismissal works from any magnifier interaction state. Exports interactionReducer and initialMachineState for testing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -14,4 +14,8 @@ export type {
|
||||
UseInteractionStateMachineReturn,
|
||||
} from './useInteractionStateMachine'
|
||||
|
||||
export { useInteractionStateMachine } from './useInteractionStateMachine'
|
||||
export {
|
||||
initialMachineState,
|
||||
interactionReducer,
|
||||
useInteractionStateMachine,
|
||||
} from './useInteractionStateMachine'
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
/**
|
||||
* Unit tests for Interaction State Machine
|
||||
*
|
||||
* Tests state transitions, context updates, and edge cases
|
||||
* to verify the state machine behaves correctly before migration.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { InteractionEvent, MachineState } from './useInteractionStateMachine'
|
||||
import { initialMachineState, interactionReducer } from './useInteractionStateMachine'
|
||||
|
||||
// Helper to apply multiple events in sequence
|
||||
function applyEvents(events: InteractionEvent[]): MachineState {
|
||||
return events.reduce((state, event) => interactionReducer(state, event), initialMachineState)
|
||||
}
|
||||
|
||||
describe('interactionReducer', () => {
|
||||
// ===========================================================================
|
||||
// Initial State
|
||||
// ===========================================================================
|
||||
describe('initial state', () => {
|
||||
it('starts in IDLE state', () => {
|
||||
expect(initialMachineState.state).toBe('IDLE')
|
||||
})
|
||||
|
||||
it('has null cursor position', () => {
|
||||
expect(initialMachineState.context.cursorPosition).toBeNull()
|
||||
})
|
||||
|
||||
it('has default zoom of 1', () => {
|
||||
expect(initialMachineState.context.currentZoom).toBe(1)
|
||||
expect(initialMachineState.context.targetZoom).toBe(1)
|
||||
})
|
||||
|
||||
it('has no previous state', () => {
|
||||
expect(initialMachineState.previousState).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
// IDLE State Transitions
|
||||
// ===========================================================================
|
||||
describe('IDLE state', () => {
|
||||
it('transitions to HOVERING on MOUSE_ENTER', () => {
|
||||
const result = interactionReducer(initialMachineState, { type: 'MOUSE_ENTER' })
|
||||
expect(result.state).toBe('HOVERING')
|
||||
expect(result.previousState).toBe('IDLE')
|
||||
})
|
||||
|
||||
it('transitions to MAGNIFIER_VISIBLE on SHOW_MAGNIFIER', () => {
|
||||
const result = interactionReducer(initialMachineState, { type: 'SHOW_MAGNIFIER' })
|
||||
expect(result.state).toBe('MAGNIFIER_VISIBLE')
|
||||
expect(result.context.targetOpacity).toBe(1)
|
||||
})
|
||||
|
||||
it('stores touch start position on map TOUCH_START', () => {
|
||||
const result = interactionReducer(initialMachineState, {
|
||||
type: 'TOUCH_START',
|
||||
target: 'map',
|
||||
touches: [{ x: 100, y: 200, identifier: 1 }],
|
||||
})
|
||||
// Stays IDLE until drag threshold met
|
||||
expect(result.state).toBe('IDLE')
|
||||
expect(result.context.touchStart).toEqual({ x: 100, y: 200, identifier: 1 })
|
||||
})
|
||||
|
||||
it('ignores MOUSE_LEAVE', () => {
|
||||
const result = interactionReducer(initialMachineState, { type: 'MOUSE_LEAVE' })
|
||||
expect(result).toBe(initialMachineState) // Same reference = no change
|
||||
})
|
||||
|
||||
it('ignores DISMISS_MAGNIFIER', () => {
|
||||
const result = interactionReducer(initialMachineState, { type: 'DISMISS_MAGNIFIER' })
|
||||
expect(result).toBe(initialMachineState)
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
// HOVERING State Transitions
|
||||
// ===========================================================================
|
||||
describe('HOVERING state', () => {
|
||||
const hoveringState = applyEvents([{ type: 'MOUSE_ENTER' }])
|
||||
|
||||
it('transitions to IDLE on MOUSE_LEAVE', () => {
|
||||
const result = interactionReducer(hoveringState, { type: 'MOUSE_LEAVE' })
|
||||
expect(result.state).toBe('IDLE')
|
||||
expect(result.context.cursorPosition).toBeNull()
|
||||
})
|
||||
|
||||
it('updates cursor position on MOUSE_MOVE', () => {
|
||||
const result = interactionReducer(hoveringState, {
|
||||
type: 'MOUSE_MOVE',
|
||||
position: { x: 150, y: 250 },
|
||||
movement: { dx: 5, dy: 5 },
|
||||
})
|
||||
expect(result.state).toBe('HOVERING')
|
||||
expect(result.context.cursorPosition).toEqual({ x: 150, y: 250 })
|
||||
})
|
||||
|
||||
it('transitions to MAP_PANNING_DESKTOP on middle MOUSE_DOWN', () => {
|
||||
const result = interactionReducer(hoveringState, {
|
||||
type: 'MOUSE_DOWN',
|
||||
button: 'middle',
|
||||
})
|
||||
expect(result.state).toBe('MAP_PANNING_DESKTOP')
|
||||
})
|
||||
|
||||
it('ignores left MOUSE_DOWN', () => {
|
||||
const result = interactionReducer(hoveringState, {
|
||||
type: 'MOUSE_DOWN',
|
||||
button: 'left',
|
||||
})
|
||||
expect(result.state).toBe('HOVERING')
|
||||
})
|
||||
|
||||
it('transitions to MAGNIFIER_VISIBLE on SHOW_MAGNIFIER', () => {
|
||||
const result = interactionReducer(hoveringState, { type: 'SHOW_MAGNIFIER' })
|
||||
expect(result.state).toBe('MAGNIFIER_VISIBLE')
|
||||
expect(result.context.targetOpacity).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
// MAGNIFIER_VISIBLE State Transitions
|
||||
// ===========================================================================
|
||||
describe('MAGNIFIER_VISIBLE state', () => {
|
||||
const magnifierState = applyEvents([{ type: 'MOUSE_ENTER' }, { type: 'SHOW_MAGNIFIER' }])
|
||||
|
||||
it('updates cursor position on MOUSE_MOVE', () => {
|
||||
const result = interactionReducer(magnifierState, {
|
||||
type: 'MOUSE_MOVE',
|
||||
position: { x: 200, y: 300 },
|
||||
movement: { dx: 10, dy: 10 },
|
||||
})
|
||||
expect(result.state).toBe('MAGNIFIER_VISIBLE')
|
||||
expect(result.context.cursorPosition).toEqual({ x: 200, y: 300 })
|
||||
})
|
||||
|
||||
it('transitions to IDLE on DISMISS_MAGNIFIER', () => {
|
||||
const result = interactionReducer(magnifierState, { type: 'DISMISS_MAGNIFIER' })
|
||||
expect(result.state).toBe('IDLE')
|
||||
expect(result.context.targetOpacity).toBe(0)
|
||||
})
|
||||
|
||||
it('transitions to PRECISION_MODE on REQUEST_PRECISION', () => {
|
||||
const result = interactionReducer(magnifierState, { type: 'REQUEST_PRECISION' })
|
||||
expect(result.state).toBe('PRECISION_MODE')
|
||||
})
|
||||
|
||||
it('transitions to MAGNIFIER_EXPANDED on EXPAND_MAGNIFIER', () => {
|
||||
const result = interactionReducer(magnifierState, { type: 'EXPAND_MAGNIFIER' })
|
||||
expect(result.state).toBe('MAGNIFIER_EXPANDED')
|
||||
})
|
||||
|
||||
it('transitions to MAGNIFIER_PANNING on single-finger magnifier TOUCH_START', () => {
|
||||
const result = interactionReducer(magnifierState, {
|
||||
type: 'TOUCH_START',
|
||||
target: 'magnifier',
|
||||
touches: [{ x: 50, y: 50, identifier: 1 }],
|
||||
})
|
||||
expect(result.state).toBe('MAGNIFIER_PANNING')
|
||||
expect(result.context.touchStart).toEqual({ x: 50, y: 50, identifier: 1 })
|
||||
})
|
||||
|
||||
it('transitions to MAGNIFIER_PINCHING on two-finger magnifier TOUCH_START', () => {
|
||||
const result = interactionReducer(magnifierState, {
|
||||
type: 'TOUCH_START',
|
||||
target: 'magnifier',
|
||||
touches: [
|
||||
{ x: 50, y: 50, identifier: 1 },
|
||||
{ x: 100, y: 50, identifier: 2 },
|
||||
],
|
||||
})
|
||||
expect(result.state).toBe('MAGNIFIER_PINCHING')
|
||||
// Pinch start distance should be calculated
|
||||
expect(result.context.pinchStartDistance).toBe(50) // sqrt((100-50)^2 + (50-50)^2)
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
// MAGNIFIER_PANNING State Transitions
|
||||
// ===========================================================================
|
||||
describe('MAGNIFIER_PANNING state', () => {
|
||||
const panningState = applyEvents([
|
||||
{ type: 'MOUSE_ENTER' },
|
||||
{ type: 'SHOW_MAGNIFIER' },
|
||||
{
|
||||
type: 'TOUCH_START',
|
||||
target: 'magnifier',
|
||||
touches: [{ x: 50, y: 50, identifier: 1 }],
|
||||
},
|
||||
])
|
||||
|
||||
it('updates cursor position on TOUCH_MOVE', () => {
|
||||
const result = interactionReducer(panningState, {
|
||||
type: 'TOUCH_MOVE',
|
||||
target: 'magnifier',
|
||||
touches: [{ x: 75, y: 100, identifier: 1 }],
|
||||
})
|
||||
expect(result.state).toBe('MAGNIFIER_PANNING')
|
||||
expect(result.context.cursorPosition).toEqual({ x: 75, y: 100, identifier: 1 })
|
||||
})
|
||||
|
||||
it('transitions to MAGNIFIER_VISIBLE on TOUCH_END', () => {
|
||||
const result = interactionReducer(panningState, {
|
||||
type: 'TOUCH_END',
|
||||
target: 'magnifier',
|
||||
remainingTouches: 0,
|
||||
})
|
||||
expect(result.state).toBe('MAGNIFIER_VISIBLE')
|
||||
})
|
||||
|
||||
it('transitions to IDLE on DISMISS_MAGNIFIER', () => {
|
||||
const result = interactionReducer(panningState, { type: 'DISMISS_MAGNIFIER' })
|
||||
expect(result.state).toBe('IDLE')
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
// MAGNIFIER_PINCHING State Transitions
|
||||
// ===========================================================================
|
||||
describe('MAGNIFIER_PINCHING state', () => {
|
||||
const pinchingState = applyEvents([
|
||||
{ type: 'MOUSE_ENTER' },
|
||||
{ type: 'SHOW_MAGNIFIER' },
|
||||
{
|
||||
type: 'TOUCH_START',
|
||||
target: 'magnifier',
|
||||
touches: [
|
||||
{ x: 50, y: 50, identifier: 1 },
|
||||
{ x: 100, y: 50, identifier: 2 },
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
it('stays in MAGNIFIER_PINCHING on TOUCH_MOVE with two fingers', () => {
|
||||
const result = interactionReducer(pinchingState, {
|
||||
type: 'TOUCH_MOVE',
|
||||
target: 'magnifier',
|
||||
touches: [
|
||||
{ x: 40, y: 50, identifier: 1 },
|
||||
{ x: 110, y: 50, identifier: 2 },
|
||||
],
|
||||
})
|
||||
expect(result.state).toBe('MAGNIFIER_PINCHING')
|
||||
})
|
||||
|
||||
it('transitions to MAGNIFIER_VISIBLE on TOUCH_END', () => {
|
||||
const result = interactionReducer(pinchingState, {
|
||||
type: 'TOUCH_END',
|
||||
target: 'magnifier',
|
||||
remainingTouches: 0,
|
||||
})
|
||||
expect(result.state).toBe('MAGNIFIER_VISIBLE')
|
||||
})
|
||||
|
||||
it('transitions to MAGNIFIER_EXPANDED on ZOOM_THRESHOLD_REACHED', () => {
|
||||
const result = interactionReducer(pinchingState, { type: 'ZOOM_THRESHOLD_REACHED' })
|
||||
expect(result.state).toBe('MAGNIFIER_EXPANDED')
|
||||
})
|
||||
|
||||
it('transitions to IDLE on DISMISS_MAGNIFIER', () => {
|
||||
const result = interactionReducer(pinchingState, { type: 'DISMISS_MAGNIFIER' })
|
||||
expect(result.state).toBe('IDLE')
|
||||
expect(result.context.pinchStartDistance).toBeNull()
|
||||
expect(result.context.pinchStartZoom).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
// MAGNIFIER_EXPANDED State Transitions
|
||||
// ===========================================================================
|
||||
describe('MAGNIFIER_EXPANDED state', () => {
|
||||
const expandedState = applyEvents([
|
||||
{ type: 'MOUSE_ENTER' },
|
||||
{ type: 'SHOW_MAGNIFIER' },
|
||||
{ type: 'EXPAND_MAGNIFIER' },
|
||||
])
|
||||
|
||||
it('transitions to MAGNIFIER_VISIBLE on COLLAPSE_MAGNIFIER', () => {
|
||||
const result = interactionReducer(expandedState, { type: 'COLLAPSE_MAGNIFIER' })
|
||||
expect(result.state).toBe('MAGNIFIER_VISIBLE')
|
||||
})
|
||||
|
||||
it('transitions to IDLE on DISMISS_MAGNIFIER', () => {
|
||||
const result = interactionReducer(expandedState, { type: 'DISMISS_MAGNIFIER' })
|
||||
expect(result.state).toBe('IDLE')
|
||||
expect(result.context.targetOpacity).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
// MAP_PANNING_MOBILE State Transitions
|
||||
// ===========================================================================
|
||||
describe('MAP_PANNING_MOBILE state', () => {
|
||||
// This state requires SHOW_MAGNIFIER from IDLE with drag context
|
||||
const mobilePanningState: MachineState = {
|
||||
state: 'MAP_PANNING_MOBILE',
|
||||
context: {
|
||||
...initialMachineState.context,
|
||||
touchStart: { x: 100, y: 100 },
|
||||
cursorPosition: { x: 150, y: 150 },
|
||||
},
|
||||
previousState: 'IDLE',
|
||||
}
|
||||
|
||||
it('updates cursor position on TOUCH_MOVE', () => {
|
||||
const result = interactionReducer(mobilePanningState, {
|
||||
type: 'TOUCH_MOVE',
|
||||
target: 'map',
|
||||
touches: [{ x: 200, y: 250, identifier: 1 }],
|
||||
})
|
||||
expect(result.state).toBe('MAP_PANNING_MOBILE')
|
||||
expect(result.context.cursorPosition).toEqual({ x: 200, y: 250, identifier: 1 })
|
||||
})
|
||||
|
||||
it('transitions to MAGNIFIER_VISIBLE on TOUCH_END with dragTriggeredMagnifier', () => {
|
||||
const result = interactionReducer(mobilePanningState, {
|
||||
type: 'TOUCH_END',
|
||||
target: 'map',
|
||||
remainingTouches: 0,
|
||||
})
|
||||
expect(result.state).toBe('MAGNIFIER_VISIBLE')
|
||||
expect(result.context.dragTriggeredMagnifier).toBe(true)
|
||||
})
|
||||
|
||||
it('transitions to IDLE on DISMISS_MAGNIFIER', () => {
|
||||
const result = interactionReducer(mobilePanningState, { type: 'DISMISS_MAGNIFIER' })
|
||||
expect(result.state).toBe('IDLE')
|
||||
expect(result.context.dragTriggeredMagnifier).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
// MAP_PANNING_DESKTOP State Transitions
|
||||
// ===========================================================================
|
||||
describe('MAP_PANNING_DESKTOP state', () => {
|
||||
const desktopPanningState = applyEvents([
|
||||
{ type: 'MOUSE_ENTER' },
|
||||
{ type: 'MOUSE_DOWN', button: 'middle' },
|
||||
])
|
||||
|
||||
it('transitions to HOVERING on MOUSE_UP', () => {
|
||||
const result = interactionReducer(desktopPanningState, { type: 'MOUSE_UP' })
|
||||
expect(result.state).toBe('HOVERING')
|
||||
})
|
||||
|
||||
it('transitions to IDLE on MOUSE_LEAVE', () => {
|
||||
const result = interactionReducer(desktopPanningState, { type: 'MOUSE_LEAVE' })
|
||||
expect(result.state).toBe('IDLE')
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
// PRECISION_MODE State Transitions
|
||||
// ===========================================================================
|
||||
describe('PRECISION_MODE state', () => {
|
||||
const precisionState = applyEvents([
|
||||
{ type: 'MOUSE_ENTER' },
|
||||
{ type: 'SHOW_MAGNIFIER' },
|
||||
{ type: 'REQUEST_PRECISION' },
|
||||
])
|
||||
|
||||
it('updates cursor position on MOUSE_MOVE', () => {
|
||||
const result = interactionReducer(precisionState, {
|
||||
type: 'MOUSE_MOVE',
|
||||
position: { x: 300, y: 400 },
|
||||
movement: { dx: 2, dy: 3 },
|
||||
})
|
||||
expect(result.state).toBe('PRECISION_MODE')
|
||||
expect(result.context.cursorPosition).toEqual({ x: 300, y: 400 })
|
||||
})
|
||||
|
||||
it('transitions to RELEASING_PRECISION on PRECISION_ESCAPE_BOUNDARY', () => {
|
||||
const result = interactionReducer(precisionState, { type: 'PRECISION_ESCAPE_BOUNDARY' })
|
||||
expect(result.state).toBe('RELEASING_PRECISION')
|
||||
})
|
||||
|
||||
it('transitions to RELEASING_PRECISION on EXIT_PRECISION', () => {
|
||||
const result = interactionReducer(precisionState, { type: 'EXIT_PRECISION' })
|
||||
expect(result.state).toBe('RELEASING_PRECISION')
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
// RELEASING_PRECISION State Transitions
|
||||
// ===========================================================================
|
||||
describe('RELEASING_PRECISION state', () => {
|
||||
const releasingState = applyEvents([
|
||||
{ type: 'MOUSE_ENTER' },
|
||||
{ type: 'SHOW_MAGNIFIER' },
|
||||
{ type: 'REQUEST_PRECISION' },
|
||||
{ type: 'PRECISION_ESCAPE_BOUNDARY' },
|
||||
])
|
||||
|
||||
it('transitions to MAGNIFIER_VISIBLE on RELEASE_ANIMATION_DONE', () => {
|
||||
const result = interactionReducer(releasingState, { type: 'RELEASE_ANIMATION_DONE' })
|
||||
expect(result.state).toBe('MAGNIFIER_VISIBLE')
|
||||
expect(result.context.initialCapturePosition).toBeNull()
|
||||
expect(result.context.cursorSquish).toEqual({ x: 1, y: 1 })
|
||||
})
|
||||
|
||||
it('ignores other events during release animation', () => {
|
||||
const result = interactionReducer(releasingState, { type: 'MOUSE_LEAVE' })
|
||||
expect(result.state).toBe('RELEASING_PRECISION')
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
// Context Preservation
|
||||
// ===========================================================================
|
||||
describe('context preservation', () => {
|
||||
it('preserves unrelated context fields during state transitions', () => {
|
||||
const stateWithContext: MachineState = {
|
||||
...initialMachineState,
|
||||
context: {
|
||||
...initialMachineState.context,
|
||||
currentZoom: 5,
|
||||
targetZoom: 10,
|
||||
magnifierTop: 100,
|
||||
magnifierLeft: 200,
|
||||
},
|
||||
}
|
||||
|
||||
const result = interactionReducer(stateWithContext, { type: 'MOUSE_ENTER' })
|
||||
|
||||
expect(result.context.currentZoom).toBe(5)
|
||||
expect(result.context.targetZoom).toBe(10)
|
||||
expect(result.context.magnifierTop).toBe(100)
|
||||
expect(result.context.magnifierLeft).toBe(200)
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
// Previous State Tracking
|
||||
// ===========================================================================
|
||||
describe('previous state tracking', () => {
|
||||
it('tracks previous state through transitions', () => {
|
||||
let state = initialMachineState
|
||||
expect(state.previousState).toBeNull()
|
||||
|
||||
state = interactionReducer(state, { type: 'MOUSE_ENTER' })
|
||||
expect(state.state).toBe('HOVERING')
|
||||
expect(state.previousState).toBe('IDLE')
|
||||
|
||||
state = interactionReducer(state, { type: 'SHOW_MAGNIFIER' })
|
||||
expect(state.state).toBe('MAGNIFIER_VISIBLE')
|
||||
expect(state.previousState).toBe('HOVERING')
|
||||
|
||||
state = interactionReducer(state, { type: 'REQUEST_PRECISION' })
|
||||
expect(state.state).toBe('PRECISION_MODE')
|
||||
expect(state.previousState).toBe('MAGNIFIER_VISIBLE')
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
// Invalid Transitions (Should Not Change State)
|
||||
// ===========================================================================
|
||||
describe('invalid transitions', () => {
|
||||
it('ignores EXPAND_MAGNIFIER from IDLE', () => {
|
||||
const result = interactionReducer(initialMachineState, { type: 'EXPAND_MAGNIFIER' })
|
||||
expect(result).toBe(initialMachineState)
|
||||
})
|
||||
|
||||
it('ignores REQUEST_PRECISION from IDLE', () => {
|
||||
const result = interactionReducer(initialMachineState, { type: 'REQUEST_PRECISION' })
|
||||
expect(result).toBe(initialMachineState)
|
||||
})
|
||||
|
||||
it('ignores COLLAPSE_MAGNIFIER from HOVERING', () => {
|
||||
const hoveringState = applyEvents([{ type: 'MOUSE_ENTER' }])
|
||||
const result = interactionReducer(hoveringState, { type: 'COLLAPSE_MAGNIFIER' })
|
||||
expect(result).toBe(hoveringState)
|
||||
})
|
||||
|
||||
it('ignores RELEASE_ANIMATION_DONE from PRECISION_MODE', () => {
|
||||
const precisionState = applyEvents([
|
||||
{ type: 'MOUSE_ENTER' },
|
||||
{ type: 'SHOW_MAGNIFIER' },
|
||||
{ type: 'REQUEST_PRECISION' },
|
||||
])
|
||||
const result = interactionReducer(precisionState, { type: 'RELEASE_ANIMATION_DONE' })
|
||||
expect(result).toBe(precisionState)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -143,7 +143,8 @@ const initialContext: InteractionContext = {
|
||||
dragTriggeredMagnifier: false,
|
||||
}
|
||||
|
||||
const initialState: MachineState = {
|
||||
// Exported for testing
|
||||
export const initialMachineState: MachineState = {
|
||||
state: 'IDLE',
|
||||
context: initialContext,
|
||||
previousState: null,
|
||||
@@ -156,8 +157,9 @@ const initialState: MachineState = {
|
||||
/**
|
||||
* Pure reducer function that handles state transitions.
|
||||
* All state changes go through here, making the logic centralized and testable.
|
||||
* Exported for unit testing.
|
||||
*/
|
||||
function interactionReducer(machine: MachineState, event: InteractionEvent): MachineState {
|
||||
export function interactionReducer(machine: MachineState, event: InteractionEvent): MachineState {
|
||||
const { state, context } = machine
|
||||
|
||||
switch (state) {
|
||||
@@ -357,6 +359,18 @@ function interactionReducer(machine: MachineState, event: InteractionEvent): Mac
|
||||
previousState: state,
|
||||
}
|
||||
|
||||
case 'DISMISS_MAGNIFIER':
|
||||
return {
|
||||
...machine,
|
||||
state: 'IDLE',
|
||||
context: {
|
||||
...context,
|
||||
targetOpacity: 0,
|
||||
touchStart: null,
|
||||
},
|
||||
previousState: state,
|
||||
}
|
||||
|
||||
default:
|
||||
return machine
|
||||
}
|
||||
@@ -411,6 +425,19 @@ function interactionReducer(machine: MachineState, event: InteractionEvent): Mac
|
||||
previousState: state,
|
||||
}
|
||||
|
||||
case 'DISMISS_MAGNIFIER':
|
||||
return {
|
||||
...machine,
|
||||
state: 'IDLE',
|
||||
context: {
|
||||
...context,
|
||||
targetOpacity: 0,
|
||||
pinchStartDistance: null,
|
||||
pinchStartZoom: null,
|
||||
},
|
||||
previousState: state,
|
||||
}
|
||||
|
||||
default:
|
||||
return machine
|
||||
}
|
||||
@@ -628,7 +655,7 @@ export interface UseInteractionStateMachineReturn {
|
||||
* Hook that provides the interaction state machine.
|
||||
*/
|
||||
export function useInteractionStateMachine(): UseInteractionStateMachineReturn {
|
||||
const [machine, dispatch] = useReducer(interactionReducer, initialState)
|
||||
const [machine, dispatch] = useReducer(interactionReducer, initialMachineState)
|
||||
|
||||
// Create stable send function
|
||||
const send = useCallback((event: InteractionEvent) => {
|
||||
|
||||
Reference in New Issue
Block a user