Skip to content

Commit caa013f

Browse files
Fix deck state transitions. Add test cases for deck state reducer. (#1290)
* Fix deck state transitions. Add test cases for deck state reducer. * Annotate the tests explaining each action. * Remove debugging console log. * Remove temporary stepper from example. * Changeset
1 parent 6153f1a commit caa013f

File tree

5 files changed

+224
-5
lines changed

5 files changed

+224
-5
lines changed

.changeset/heavy-knives-sniff.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'spectacle': patch
3+
---
4+
5+
Fixed deck transitions for presenter mode, added test coverage around deck reducer.

packages/spectacle/src/components/presenter-mode/index.tsx

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { useRef, useCallback, useState, useEffect, ReactNode } from 'react';
1+
import {
2+
useRef,
3+
useCallback,
4+
useState,
5+
useEffect,
6+
ReactNode,
7+
ReactElement
8+
} from 'react';
29
import styled from 'styled-components';
310
import { DeckInternal, DeckRef, TemplateFn } from '../deck/deck';
411
import { Text, SpectacleLogo } from '../../index';
@@ -29,7 +36,7 @@ const PreviewSlideWrapper = styled.div<{ visible?: boolean }>(
2936
})
3037
);
3138

32-
const PresenterMode = (props: PresenterModeProps): JSX.Element => {
39+
const PresenterMode = (props: PresenterModeProps): ReactElement => {
3340
const { children, theme, backgroundImage, template } = props;
3441
const deck = useRef<DeckRef>(null);
3542
const previewDeck = useRef<DeckRef>(null);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { renderHook, act } from '@testing-library/react';
2+
import useDeckState, { DeckView } from './use-deck-state';
3+
4+
describe('useDeckState', () => {
5+
const initialState: DeckView = {
6+
slideIndex: 1,
7+
stepIndex: 1
8+
};
9+
10+
/**
11+
* The INITIALIZE_TO should set the active and pending views
12+
* to the values provided in the payload.
13+
*/
14+
it('should handle INITIALIZE_TO action', () => {
15+
const { result } = renderHook(() => useDeckState(initialState));
16+
17+
act(() => {
18+
result.current.initializeTo({ slideIndex: 2, stepIndex: 2 });
19+
});
20+
21+
expect(result.current.activeView).toEqual({ slideIndex: 2, stepIndex: 2 });
22+
expect(result.current.pendingView).toEqual({ slideIndex: 2, stepIndex: 2 });
23+
expect(result.current.initialized).toBe(true);
24+
});
25+
26+
/**
27+
* The SKIP_TO action should set the pending view slide index to
28+
* the slide index provided by the payload and set the navigation
29+
* direction based on a delta of the previous and pending slides.
30+
*/
31+
it('should handle SKIP_TO action', () => {
32+
const { result } = renderHook(() => useDeckState(initialState));
33+
34+
act(() => {
35+
result.current.skipTo({ slideIndex: 3 });
36+
});
37+
38+
expect(result.current.navigationDirection).toBe(1);
39+
expect(result.current.pendingView.slideIndex).toBe(3);
40+
});
41+
42+
it('should handle SKIP_TO action in reverse', () => {
43+
const { result } = renderHook(() =>
44+
useDeckState({ slideIndex: 5, stepIndex: 0, slideId: 0 })
45+
);
46+
47+
act(() => {
48+
result.current.skipTo({ slideIndex: 3 });
49+
});
50+
51+
expect(result.current.navigationDirection).toBe(-1);
52+
expect(result.current.pendingView.slideIndex).toBe(3);
53+
});
54+
55+
/**
56+
* The STEP_FORWARD action should increment the pending slide index by 1
57+
* and have a positive navigation direction.
58+
*/
59+
it('should handle STEP_FORWARD action', () => {
60+
const { result } = renderHook(() => useDeckState(initialState));
61+
62+
act(() => {
63+
result.current.stepForward();
64+
});
65+
66+
expect(result.current.pendingView.stepIndex).toBe(
67+
initialState.stepIndex + 1
68+
);
69+
expect(result.current.navigationDirection).toBe(1);
70+
});
71+
72+
/**
73+
* The STEP_FORWARD action should decrement the pending slide index by 1
74+
* and have a negative navigation direction.
75+
*/
76+
it('should handle STEP_BACKWARD action', () => {
77+
const { result } = renderHook(() => useDeckState(initialState));
78+
79+
act(() => {
80+
result.current.stepBackward();
81+
});
82+
83+
expect(result.current.pendingView.stepIndex).toBe(
84+
initialState.stepIndex - 1
85+
);
86+
expect(result.current.navigationDirection).toBe(-1);
87+
});
88+
89+
/**
90+
* The ADVANCE_SLIDE action should increment the pending slide index by 1,
91+
* reset the step index, and have a positive navigation direction.
92+
*/
93+
it('should handle ADVANCE_SLIDE action', () => {
94+
const { result } = renderHook(() => useDeckState(initialState));
95+
96+
act(() => {
97+
result.current.advanceSlide();
98+
});
99+
100+
expect(result.current.pendingView.slideIndex).toBe(
101+
initialState.slideIndex + 1
102+
);
103+
expect(result.current.pendingView.stepIndex).toBe(0);
104+
expect(result.current.navigationDirection).toBe(1);
105+
});
106+
107+
/**
108+
* The REGRESS_SLIDE action should decrement the pending slide index by 1,
109+
* reset the step index, and have a negative navigation direction.
110+
*/
111+
it('should handle REGRESS_SLIDE action', () => {
112+
const { result } = renderHook(() => useDeckState(initialState));
113+
114+
act(() => {
115+
result.current.regressSlide({ stepIndex: 0 });
116+
});
117+
118+
expect(result.current.pendingView.slideIndex).toBe(
119+
initialState.slideIndex - 1
120+
);
121+
expect(result.current.pendingView.stepIndex).toBe(0);
122+
expect(result.current.navigationDirection).toBe(-1);
123+
});
124+
125+
/**
126+
* The COMMIT_TRANSITION action should set the active slide view
127+
* and pending slide view to the payload values.
128+
*/
129+
it('should handle COMMIT_TRANSITION action', () => {
130+
const { result } = renderHook(() => useDeckState(initialState));
131+
132+
act(() => {
133+
result.current.commitTransition({ slideIndex: 2, stepIndex: 2 });
134+
});
135+
136+
expect(result.current.activeView).toEqual({ slideIndex: 2, stepIndex: 2 });
137+
expect(result.current.pendingView).toEqual({ slideIndex: 2, stepIndex: 2 });
138+
});
139+
140+
/**
141+
* The CANCEL_TRANSITION action should cancel the slide transition
142+
* by reverting the pending view values to what the current active slide values are.
143+
*/
144+
it('should handle CANCEL_TRANSITION action', () => {
145+
const { result } = renderHook(() => useDeckState(initialState));
146+
147+
act(() => {
148+
result.current.cancelTransition();
149+
});
150+
151+
expect(result.current.pendingView).toEqual(result.current.activeView);
152+
});
153+
});

packages/spectacle/src/hooks/use-deck-state.ts

+19-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useReducer, useMemo } from 'react';
22
import { merge } from 'merge-anything';
33
import { SlideId } from '../components/deck/deck';
4+
import clamp from '../utils/clamp';
45

56
export const GOTO_FINAL_STEP = null as unknown as number;
67

@@ -16,7 +17,7 @@ export type DeckState = {
1617
pendingView: DeckView;
1718
};
1819

19-
const initialDeckState: DeckState = {
20+
export const initialDeckState: DeckState = {
2021
initialized: false,
2122
navigationDirection: 0,
2223
pendingView: {
@@ -49,8 +50,15 @@ function deckReducer(state: DeckState, { type, payload = {} }: ReducerActions) {
4950
initialized: true
5051
};
5152
case 'SKIP_TO':
53+
const navigationDirection = (() => {
54+
if ('slideIndex' in payload && payload.slideIndex) {
55+
return clamp(payload.slideIndex - state.activeView.slideIndex, -1, 1);
56+
}
57+
return null;
58+
})();
5259
return {
5360
...state,
61+
navigationDirection: navigationDirection || state.navigationDirection,
5462
pendingView: merge(state.pendingView, payload)
5563
};
5664
case 'STEP_FORWARD':
@@ -109,8 +117,16 @@ export default function useDeckState(userProvidedInitialState: DeckView) {
109117
{ initialized, navigationDirection, pendingView, activeView },
110118
dispatch
111119
] = useReducer(deckReducer, {
112-
...initialDeckState,
113-
...userProvidedInitialState
120+
initialized: initialDeckState.initialized,
121+
navigationDirection: initialDeckState.navigationDirection,
122+
pendingView: {
123+
...initialDeckState.pendingView,
124+
...userProvidedInitialState
125+
},
126+
activeView: {
127+
...initialDeckState.activeView,
128+
...userProvidedInitialState
129+
}
114130
});
115131
const actions = useMemo(
116132
() => ({
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import clamp, { toFiniteNumber } from './clamp';
2+
3+
describe('toFiniteNumber', () => {
4+
it('should return 0 for NaN', () => {
5+
expect(toFiniteNumber(NaN)).toBe(0);
6+
expect(toFiniteNumber(Number.NaN)).toBe(0);
7+
});
8+
9+
it('should convert finite values to finite numbers', () => {
10+
expect(toFiniteNumber(123)).toBe(123);
11+
expect(toFiniteNumber(-456.789)).toBe(-456.789);
12+
});
13+
14+
it('should return Number.MAX_SAFE_INTEGER for Infinity', () => {
15+
expect(toFiniteNumber(Infinity)).toBe(Number.MAX_SAFE_INTEGER);
16+
expect(toFiniteNumber(-Infinity)).toBe(-Number.MAX_SAFE_INTEGER);
17+
});
18+
});
19+
20+
describe('clamp', () => {
21+
it('should return NaN for NaN input', () => {
22+
expect(clamp(NaN)).toBeNaN();
23+
});
24+
25+
it('should clamp value to specified range', () => {
26+
expect(clamp(10, 0, 5)).toBe(5);
27+
expect(clamp(-3, -10, 0)).toBe(-3);
28+
expect(clamp(7, 5, 10)).toBe(7);
29+
expect(clamp(-15, -10, 10)).toBe(-10);
30+
});
31+
32+
it('should not clamp when range is not specified', () => {
33+
expect(clamp(10)).toBe(10);
34+
expect(clamp(-3)).toBe(-3);
35+
expect(clamp(7)).toBe(7);
36+
expect(clamp(-15)).toBe(-15);
37+
});
38+
});

0 commit comments

Comments
 (0)