diff --git a/src/components/Interactions/Swipe/Swipe.test.tsx b/src/components/Interactions/Swipe/Swipe.test.tsx
index a343189a..5ee71c3e 100644
--- a/src/components/Interactions/Swipe/Swipe.test.tsx
+++ b/src/components/Interactions/Swipe/Swipe.test.tsx
@@ -7,7 +7,7 @@ import { Provider } from 'react-redux';
import gamesReducer, { roundNext } from '../../../../redux/GamesSlice';
import playersReducer, { playerRoundScoreIncrement } from '../../../../redux/PlayersSlice';
-import settingsReducer, { initialState as settingsInitialState } from '../../../../redux/SettingsSlice';
+import settingsReducer, { initialState as settingsInitialState, setAddendOne } from '../../../../redux/SettingsSlice';
import SwipeVertical from './Swipe';
@@ -27,7 +27,7 @@ jest.mock('react-native-reanimated', () => ({
jest.mock('../../../Analytics', () => ({ logEvent: function () { } }));
jest.mock('expo-haptics', () => ({
- impactAsync: function () { },
+ impactAsync: jest.fn(),
ImpactFeedbackStyle: { Light: 'Light', Medium: 'Medium', Heavy: 'Heavy' },
}));
@@ -58,6 +58,7 @@ beforeEach(() => {
(globalThis as any).__ph = {};
(globalThis as any).__react = undefined;
jest.useRealTimers();
+ jest.clearAllMocks();
});
const renderSwipe = ({ onRender }: { onRender?: (id: string) => void; } = {}) => {
@@ -70,12 +71,12 @@ const renderSwipe = ({ onRender }: { onRender?: (id: string) => void; } = {}) =>
},
});
const dispatch = jest.spyOn(store, 'dispatch');
- render(
+ const view = render(
<>>
);
- return { dispatch, store };
+ return { dispatch, store, unmount: view.unmount };
};
const triggerReaction = (totalOffset: number, prevTotalOffset: number) => {
@@ -186,6 +187,49 @@ describe('SwipeVertical', () => {
);
});
+ it('cancels the original hold timer after a re-render', () => {
+ const { dispatch, store } = renderSwipe();
+ jest.useFakeTimers();
+
+ (globalThis as any).__ph.onBegin();
+ act(() => {
+ store.dispatch(setAddendOne(2));
+ });
+
+ const rerenderedPan = (globalThis as any).__ph;
+ rerenderedPan.onUpdate({ translationY: -2 });
+ triggerReaction(2, 0);
+ act(() => {
+ jest.advanceTimersByTime(401);
+ });
+
+ rerenderedPan.onUpdate({ translationY: -100 });
+ triggerReaction(100, 2);
+ act(() => {
+ rerenderedPan.onEnd({ translationY: -100 });
+ rerenderedPan.onFinalize();
+ });
+
+ expect(dispatch).toHaveBeenCalledWith(
+ playerRoundScoreIncrement('player-1', 0, 4)
+ );
+ });
+
+ it('clears the hold timer when unmounted', () => {
+ const { unmount } = renderSwipe();
+ const ph = (globalThis as any).__ph;
+ jest.useFakeTimers();
+
+ ph.onBegin();
+ unmount();
+ act(() => {
+ jest.advanceTimersByTime(401);
+ });
+
+ const haptics = jest.requireMock('expo-haptics');
+ expect(haptics.impactAsync).not.toHaveBeenCalled();
+ });
+
it('does not re-render solely because the current round changes', () => {
const onRender = jest.fn();
const { store } = renderSwipe({ onRender });
diff --git a/src/components/Interactions/Swipe/Swipe.tsx b/src/components/Interactions/Swipe/Swipe.tsx
index 22e1cecd..7f01a780 100644
--- a/src/components/Interactions/Swipe/Swipe.tsx
+++ b/src/components/Interactions/Swipe/Swipe.tsx
@@ -60,16 +60,10 @@ const SwipeVertical: React.FC = ({
const secondaryHoldTime = 500;
const holdDuration = useRef(new Animated.Value(0)).current;
- let secondaryHoldTimer: ReturnType;
+ const secondaryHoldTimerRef = useRef>();
const [secondaryHold, setSecondaryHold] = useState(false);
const isSecondaryHoldActive = useSharedValue(false);
- useEffect(() => {
- return () => {
- clearTimeout(secondaryHoldTimer);
- };
- }, []);
-
const scale = holdDuration.interpolate({
inputRange: [0, secondaryHoldTime * .2, secondaryHoldTime * .9, secondaryHoldTime],
outputRange: [1, 1, 1.1, 1.05],
@@ -80,14 +74,17 @@ const SwipeVertical: React.FC = ({
const animationRef = useRef(
);
- const secondaryHoldStart = () => {
+ const secondaryHoldStart = useCallback(() => {
+ clearTimeout(secondaryHoldTimerRef.current);
+
Animated.timing(holdDuration, {
toValue: secondaryHoldTime,
duration: secondaryHoldTime,
useNativeDriver: true,
}).start();
- secondaryHoldTimer = setTimeout(() => {
+ secondaryHoldTimerRef.current = setTimeout(() => {
+ secondaryHoldTimerRef.current = undefined;
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
setSecondaryHold(true);
isSecondaryHoldActive.value = true;
@@ -113,9 +110,12 @@ const SwipeVertical: React.FC = ({
);
animationRef.current.start();
}, secondaryHoldTime * .8);
- };
+ }, [holdDuration, isSecondaryHoldActive, wiggleValue]);
+
+ const secondaryHoldStop = useCallback(() => {
+ clearTimeout(secondaryHoldTimerRef.current);
+ secondaryHoldTimerRef.current = undefined;
- const secondaryHoldStop = () => {
Animated.timing(holdDuration, {
toValue: 0,
duration: 100,
@@ -134,8 +134,14 @@ const SwipeVertical: React.FC = ({
useNativeDriver: true,
}).start();
- clearTimeout(secondaryHoldTimer);
- };
+ }, [holdDuration, isSecondaryHoldActive, wiggleValue]);
+
+ useEffect(() => {
+ return () => {
+ clearTimeout(secondaryHoldTimerRef.current);
+ animationRef.current?.stop();
+ };
+ }, []);
//#endregion
@@ -165,7 +171,7 @@ const SwipeVertical: React.FC = ({
interaction: 'swipe-vertical',
});
secondaryHoldStop();
- }, [index, currentGameId, secondaryHold, addendOne, addendTwo, getCurrentRoundIndex, menuOpen]);
+ }, [index, currentGameId, secondaryHold, addendOne, addendTwo, getCurrentRoundIndex, menuOpen, secondaryHoldStop]);
const panGesture = Gesture.Pan()
.enabled(!currentGameLocked && !menuOpen)