@react-chess-tools/react-chess-game 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/coverage/clover.xml +6 -0
- package/coverage/coverage-final.json +1 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +101 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +196 -0
- package/coverage/lcov.info +0 -0
- package/dist/index.d.mts +32 -2
- package/dist/index.mjs +113 -55
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/ChessGame/parts/Sounds.tsx +8 -2
- package/src/hooks/__tests__/useChessGame.test.tsx +230 -0
- package/src/hooks/useBoardSounds.test.ts +196 -0
- package/src/hooks/useBoardSounds.ts +8 -3
- package/src/hooks/useChessGame.ts +92 -41
- package/src/hooks/useChessGameContext.ts +1 -1
- package/src/hooks/useKeyboardControls.test.tsx +171 -0
- package/src/index.ts +19 -0
- package/src/utils/__tests__/board.test.ts +143 -0
- package/src/utils/__tests__/chess.test.ts +198 -0
- package/src/utils/board.ts +2 -2
- package/src/utils/chess.ts +12 -8
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react";
|
|
2
|
+
import { type Move, type PieceSymbol } from "chess.js";
|
|
3
|
+
import { useBoardSounds } from "./useBoardSounds";
|
|
4
|
+
import { useChessGameContext } from "./useChessGameContext";
|
|
5
|
+
import { type Sound } from "../assets/sounds";
|
|
6
|
+
|
|
7
|
+
// Mock the context hook
|
|
8
|
+
jest.mock("./useChessGameContext");
|
|
9
|
+
const mockedUseChessGameContext = useChessGameContext as jest.MockedFunction<
|
|
10
|
+
typeof useChessGameContext
|
|
11
|
+
>;
|
|
12
|
+
|
|
13
|
+
// Helper to create mock audio elements
|
|
14
|
+
const createMockSounds = (): Record<Sound, HTMLAudioElement> => ({
|
|
15
|
+
move: { play: jest.fn() } as unknown as HTMLAudioElement,
|
|
16
|
+
capture: { play: jest.fn() } as unknown as HTMLAudioElement,
|
|
17
|
+
gameOver: { play: jest.fn() } as unknown as HTMLAudioElement,
|
|
18
|
+
check: { play: jest.fn() } as unknown as HTMLAudioElement,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("useBoardSounds", () => {
|
|
22
|
+
let mockSounds: Record<Sound, HTMLAudioElement>;
|
|
23
|
+
let mockContextValue: {
|
|
24
|
+
info: {
|
|
25
|
+
lastMove?: Partial<Move> | null;
|
|
26
|
+
isCheckmate?: boolean;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
// Reset mocks before each test
|
|
32
|
+
jest.clearAllMocks();
|
|
33
|
+
mockSounds = createMockSounds();
|
|
34
|
+
mockContextValue = {
|
|
35
|
+
info: {
|
|
36
|
+
lastMove: null,
|
|
37
|
+
isCheckmate: false,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
mockedUseChessGameContext.mockReturnValue(
|
|
41
|
+
mockContextValue as unknown as ReturnType<typeof useChessGameContext>,
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should not play any sound initially", () => {
|
|
46
|
+
renderHook(() => useBoardSounds(mockSounds));
|
|
47
|
+
expect(mockSounds.move.play).not.toHaveBeenCalled();
|
|
48
|
+
expect(mockSounds.capture.play).not.toHaveBeenCalled();
|
|
49
|
+
expect(mockSounds.gameOver.play).not.toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should play move sound when lastMove is present", () => {
|
|
53
|
+
mockContextValue.info.lastMove = {
|
|
54
|
+
from: "e2",
|
|
55
|
+
to: "e4",
|
|
56
|
+
piece: "P" as PieceSymbol,
|
|
57
|
+
} as unknown as Partial<Move>;
|
|
58
|
+
const { rerender } = renderHook(() => useBoardSounds(mockSounds));
|
|
59
|
+
|
|
60
|
+
mockedUseChessGameContext.mockReturnValue(
|
|
61
|
+
mockContextValue as unknown as ReturnType<typeof useChessGameContext>,
|
|
62
|
+
);
|
|
63
|
+
rerender();
|
|
64
|
+
|
|
65
|
+
expect(mockSounds.move.play).toHaveBeenCalledTimes(1);
|
|
66
|
+
expect(mockSounds.capture.play).not.toHaveBeenCalled();
|
|
67
|
+
expect(mockSounds.gameOver.play).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should play capture sound when lastMove includes a capture", () => {
|
|
71
|
+
mockContextValue.info.lastMove = {
|
|
72
|
+
from: "e4",
|
|
73
|
+
to: "d5",
|
|
74
|
+
piece: "P" as PieceSymbol,
|
|
75
|
+
captured: "p" as PieceSymbol,
|
|
76
|
+
} as unknown as Partial<Move>;
|
|
77
|
+
mockedUseChessGameContext.mockReturnValue(
|
|
78
|
+
mockContextValue as unknown as ReturnType<typeof useChessGameContext>,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const { rerender } = renderHook(() => useBoardSounds(mockSounds));
|
|
82
|
+
rerender();
|
|
83
|
+
|
|
84
|
+
expect(mockSounds.capture.play).toHaveBeenCalledTimes(1);
|
|
85
|
+
expect(mockSounds.move.play).not.toHaveBeenCalled();
|
|
86
|
+
expect(mockSounds.gameOver.play).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should play gameOver sound when isCheckmate is true", () => {
|
|
90
|
+
mockContextValue.info.isCheckmate = true;
|
|
91
|
+
mockContextValue.info.lastMove = {
|
|
92
|
+
from: "f3",
|
|
93
|
+
to: "g5",
|
|
94
|
+
piece: "Q" as PieceSymbol,
|
|
95
|
+
} as unknown as Partial<Move>;
|
|
96
|
+
mockedUseChessGameContext.mockReturnValue(
|
|
97
|
+
mockContextValue as unknown as ReturnType<typeof useChessGameContext>,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const { rerender } = renderHook(() => useBoardSounds(mockSounds));
|
|
101
|
+
rerender();
|
|
102
|
+
|
|
103
|
+
expect(mockSounds.gameOver.play).toHaveBeenCalledTimes(1);
|
|
104
|
+
expect(mockSounds.move.play).not.toHaveBeenCalled();
|
|
105
|
+
expect(mockSounds.capture.play).not.toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should play gameOver sound even if last move was a capture", () => {
|
|
109
|
+
mockContextValue.info.isCheckmate = true;
|
|
110
|
+
mockContextValue.info.lastMove = {
|
|
111
|
+
from: "f3",
|
|
112
|
+
to: "g7",
|
|
113
|
+
piece: "Q" as PieceSymbol,
|
|
114
|
+
captured: "p" as PieceSymbol,
|
|
115
|
+
} as unknown as Partial<Move>;
|
|
116
|
+
mockedUseChessGameContext.mockReturnValue(
|
|
117
|
+
mockContextValue as unknown as ReturnType<typeof useChessGameContext>,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const { rerender } = renderHook(() => useBoardSounds(mockSounds));
|
|
121
|
+
rerender();
|
|
122
|
+
|
|
123
|
+
expect(mockSounds.gameOver.play).toHaveBeenCalledTimes(1);
|
|
124
|
+
expect(mockSounds.capture.play).not.toHaveBeenCalled();
|
|
125
|
+
expect(mockSounds.move.play).not.toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should not play sound if lastMove becomes null", () => {
|
|
129
|
+
mockContextValue.info.lastMove = {
|
|
130
|
+
from: "e2",
|
|
131
|
+
to: "e4",
|
|
132
|
+
piece: "P" as PieceSymbol,
|
|
133
|
+
} as unknown as Partial<Move>;
|
|
134
|
+
mockedUseChessGameContext.mockReturnValue(
|
|
135
|
+
mockContextValue as unknown as ReturnType<typeof useChessGameContext>,
|
|
136
|
+
);
|
|
137
|
+
const { rerender } = renderHook(() => useBoardSounds(mockSounds));
|
|
138
|
+
rerender();
|
|
139
|
+
expect(mockSounds.move.play).toHaveBeenCalledTimes(1);
|
|
140
|
+
|
|
141
|
+
mockContextValue.info.lastMove = null;
|
|
142
|
+
mockedUseChessGameContext.mockReturnValue(
|
|
143
|
+
mockContextValue as unknown as ReturnType<typeof useChessGameContext>,
|
|
144
|
+
);
|
|
145
|
+
rerender();
|
|
146
|
+
|
|
147
|
+
expect(mockSounds.move.play).toHaveBeenCalledTimes(1);
|
|
148
|
+
expect(mockSounds.capture.play).not.toHaveBeenCalled();
|
|
149
|
+
expect(mockSounds.gameOver.play).not.toHaveBeenCalled();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should use updated sounds when they change", () => {
|
|
153
|
+
// Setup initial sounds and render hook
|
|
154
|
+
mockContextValue.info.lastMove = {
|
|
155
|
+
from: "e2",
|
|
156
|
+
to: "e4",
|
|
157
|
+
piece: "P" as PieceSymbol,
|
|
158
|
+
} as unknown as Partial<Move>;
|
|
159
|
+
mockedUseChessGameContext.mockReturnValue(
|
|
160
|
+
mockContextValue as unknown as ReturnType<typeof useChessGameContext>,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const { rerender } = renderHook((props) => useBoardSounds(props), {
|
|
164
|
+
initialProps: mockSounds,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Verify initial sound played
|
|
168
|
+
expect(mockSounds.move.play).toHaveBeenCalledTimes(1);
|
|
169
|
+
|
|
170
|
+
// Create new set of mock sounds
|
|
171
|
+
const newMockSounds = createMockSounds();
|
|
172
|
+
|
|
173
|
+
// Re-render with new sounds and trigger a move
|
|
174
|
+
rerender(newMockSounds);
|
|
175
|
+
|
|
176
|
+
// Update lastMove to trigger sound effect with new sounds
|
|
177
|
+
mockContextValue.info.lastMove = {
|
|
178
|
+
from: "e7",
|
|
179
|
+
to: "e5",
|
|
180
|
+
piece: "P" as PieceSymbol,
|
|
181
|
+
} as unknown as Partial<Move>;
|
|
182
|
+
mockedUseChessGameContext.mockReturnValue(
|
|
183
|
+
mockContextValue as unknown as ReturnType<typeof useChessGameContext>,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
rerender(newMockSounds);
|
|
187
|
+
|
|
188
|
+
// Original sounds should not be called again
|
|
189
|
+
expect(mockSounds.move.play).toHaveBeenCalledTimes(1);
|
|
190
|
+
|
|
191
|
+
// New sounds should be called
|
|
192
|
+
expect(newMockSounds.move.play).toHaveBeenCalledTimes(1);
|
|
193
|
+
expect(newMockSounds.capture.play).not.toHaveBeenCalled();
|
|
194
|
+
expect(newMockSounds.gameOver.play).not.toHaveBeenCalled();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -6,17 +6,22 @@ export const useBoardSounds = (sounds: Record<Sound, HTMLAudioElement>) => {
|
|
|
6
6
|
const {
|
|
7
7
|
info: { lastMove, isCheckmate },
|
|
8
8
|
} = useChessGameContext();
|
|
9
|
+
|
|
9
10
|
useEffect(() => {
|
|
11
|
+
if (Object.keys(sounds).length === 0) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
if (isCheckmate) {
|
|
11
|
-
sounds.gameOver
|
|
16
|
+
sounds.gameOver?.play();
|
|
12
17
|
return;
|
|
13
18
|
}
|
|
14
19
|
if (lastMove?.captured) {
|
|
15
|
-
sounds.capture
|
|
20
|
+
sounds.capture?.play();
|
|
16
21
|
return;
|
|
17
22
|
}
|
|
18
23
|
if (lastMove) {
|
|
19
|
-
sounds.move
|
|
24
|
+
sounds.move?.play();
|
|
20
25
|
return;
|
|
21
26
|
}
|
|
22
27
|
}, [lastMove]);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
2
|
import { Chess, Color } from "chess.js";
|
|
3
3
|
import { cloneGame, getCurrentFen, getGameInfo } from "../utils/chess";
|
|
4
4
|
|
|
@@ -12,63 +12,93 @@ export const useChessGame = ({
|
|
|
12
12
|
orientation: initialOrientation,
|
|
13
13
|
}: useChessGameProps = {}) => {
|
|
14
14
|
const [game, setGame] = React.useState(new Chess(fen));
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
setGame(new Chess(fen));
|
|
18
|
+
}, [fen]);
|
|
19
|
+
|
|
15
20
|
const [orientation, setOrientation] = React.useState<Color>(
|
|
16
21
|
initialOrientation ?? "w",
|
|
17
22
|
);
|
|
18
23
|
const [currentMoveIndex, setCurrentMoveIndex] = React.useState(-1);
|
|
19
24
|
|
|
20
25
|
const history = React.useMemo(() => game.history(), [game]);
|
|
21
|
-
const isLatestMove =
|
|
22
|
-
currentMoveIndex === history.length - 1 || currentMoveIndex === -1
|
|
26
|
+
const isLatestMove = React.useMemo(
|
|
27
|
+
() => currentMoveIndex === history.length - 1 || currentMoveIndex === -1,
|
|
28
|
+
[currentMoveIndex, history.length],
|
|
29
|
+
);
|
|
23
30
|
|
|
24
|
-
const
|
|
31
|
+
const info = React.useMemo(
|
|
32
|
+
() => getGameInfo(game, orientation),
|
|
33
|
+
[game, orientation],
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const currentFen = React.useMemo(
|
|
37
|
+
() => getCurrentFen(fen, game, currentMoveIndex),
|
|
38
|
+
[game, currentMoveIndex],
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const currentPosition = React.useMemo(
|
|
42
|
+
() => game.history()[currentMoveIndex],
|
|
43
|
+
[game, currentMoveIndex],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const setPosition = React.useCallback((fen: string, orientation: Color) => {
|
|
25
47
|
const newGame = new Chess();
|
|
26
48
|
newGame.load(fen);
|
|
27
49
|
setOrientation(orientation);
|
|
28
50
|
setGame(newGame);
|
|
29
51
|
setCurrentMoveIndex(-1);
|
|
30
|
-
};
|
|
52
|
+
}, []);
|
|
31
53
|
|
|
32
|
-
const makeMove = (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
const copy = cloneGame(game);
|
|
40
|
-
copy.move(move);
|
|
41
|
-
setGame(copy);
|
|
42
|
-
setCurrentMoveIndex(copy.history().length - 1);
|
|
43
|
-
return true;
|
|
44
|
-
} catch (e) {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
};
|
|
54
|
+
const makeMove = React.useCallback(
|
|
55
|
+
(move: Parameters<Chess["move"]>[0]): boolean => {
|
|
56
|
+
// Only allow moves when we're at the latest position
|
|
57
|
+
if (!isLatestMove) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
48
60
|
|
|
49
|
-
|
|
61
|
+
try {
|
|
62
|
+
const copy = cloneGame(game);
|
|
63
|
+
copy.move(move);
|
|
64
|
+
setGame(copy);
|
|
65
|
+
setCurrentMoveIndex(copy.history().length - 1);
|
|
66
|
+
return true;
|
|
67
|
+
} catch (e) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
[isLatestMove, game],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const flipBoard = React.useCallback(() => {
|
|
50
75
|
setOrientation((orientation) => (orientation === "w" ? "b" : "w"));
|
|
51
|
-
};
|
|
76
|
+
}, []);
|
|
52
77
|
|
|
53
|
-
const goToMove = (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
78
|
+
const goToMove = React.useCallback(
|
|
79
|
+
(moveIndex: number) => {
|
|
80
|
+
if (moveIndex < -1 || moveIndex >= history.length) return;
|
|
81
|
+
setCurrentMoveIndex(moveIndex);
|
|
82
|
+
},
|
|
83
|
+
[history.length],
|
|
84
|
+
);
|
|
57
85
|
|
|
58
|
-
const goToStart = () => goToMove(-1);
|
|
59
|
-
const goToEnd = (
|
|
60
|
-
|
|
61
|
-
|
|
86
|
+
const goToStart = React.useCallback(() => goToMove(-1), []);
|
|
87
|
+
const goToEnd = React.useCallback(
|
|
88
|
+
() => goToMove(history.length - 1),
|
|
89
|
+
[history.length],
|
|
90
|
+
);
|
|
91
|
+
const goToPreviousMove = React.useCallback(
|
|
92
|
+
() => goToMove(currentMoveIndex - 1),
|
|
93
|
+
[currentMoveIndex],
|
|
94
|
+
);
|
|
95
|
+
const goToNextMove = React.useCallback(
|
|
96
|
+
() => goToMove(currentMoveIndex + 1),
|
|
97
|
+
[currentMoveIndex],
|
|
98
|
+
);
|
|
62
99
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
currentFen: getCurrentFen(fen, game, currentMoveIndex),
|
|
66
|
-
currentPosition: game.history()[currentMoveIndex],
|
|
67
|
-
orientation,
|
|
68
|
-
currentMoveIndex,
|
|
69
|
-
isLatestMove,
|
|
70
|
-
info: getGameInfo(game, orientation),
|
|
71
|
-
methods: {
|
|
100
|
+
const methods = React.useMemo(
|
|
101
|
+
() => ({
|
|
72
102
|
makeMove,
|
|
73
103
|
setPosition,
|
|
74
104
|
flipBoard,
|
|
@@ -77,6 +107,27 @@ export const useChessGame = ({
|
|
|
77
107
|
goToEnd,
|
|
78
108
|
goToPreviousMove,
|
|
79
109
|
goToNextMove,
|
|
80
|
-
},
|
|
110
|
+
}),
|
|
111
|
+
[
|
|
112
|
+
makeMove,
|
|
113
|
+
setPosition,
|
|
114
|
+
flipBoard,
|
|
115
|
+
goToMove,
|
|
116
|
+
goToStart,
|
|
117
|
+
goToEnd,
|
|
118
|
+
goToPreviousMove,
|
|
119
|
+
goToNextMove,
|
|
120
|
+
],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
game,
|
|
125
|
+
currentFen,
|
|
126
|
+
currentPosition,
|
|
127
|
+
orientation,
|
|
128
|
+
currentMoveIndex,
|
|
129
|
+
isLatestMove,
|
|
130
|
+
info,
|
|
131
|
+
methods,
|
|
81
132
|
};
|
|
82
133
|
};
|
|
@@ -9,7 +9,7 @@ export const useChessGameContext = () => {
|
|
|
9
9
|
const context = React.useContext(ChessGameContext);
|
|
10
10
|
if (!context) {
|
|
11
11
|
throw new Error(
|
|
12
|
-
|
|
12
|
+
`useChessGameContext must be used within a ChessGame component. Make sure your component is wrapped with <ChessGame.Root> or ensure the ChessGame component is properly rendered in the component tree.`,
|
|
13
13
|
);
|
|
14
14
|
}
|
|
15
15
|
return context;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { renderHook, act } from "@testing-library/react";
|
|
2
|
+
import { useKeyboardControls } from "./useKeyboardControls";
|
|
3
|
+
import {
|
|
4
|
+
useChessGameContext,
|
|
5
|
+
ChessGameContextType,
|
|
6
|
+
} from "./useChessGameContext";
|
|
7
|
+
import { Chess, Color } from "chess.js";
|
|
8
|
+
|
|
9
|
+
// Mock the context hook
|
|
10
|
+
jest.mock("./useChessGameContext");
|
|
11
|
+
const mockUseChessGameContext = useChessGameContext as jest.MockedFunction<
|
|
12
|
+
typeof useChessGameContext
|
|
13
|
+
>;
|
|
14
|
+
|
|
15
|
+
describe("useKeyboardControls", () => {
|
|
16
|
+
let mockGameContext: jest.Mocked<ChessGameContextType>;
|
|
17
|
+
let addEventListenerSpy: jest.SpyInstance;
|
|
18
|
+
let removeEventListenerSpy: jest.SpyInstance;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
// Reset mocks and spies
|
|
22
|
+
jest.clearAllMocks();
|
|
23
|
+
mockGameContext = {
|
|
24
|
+
game: {
|
|
25
|
+
/* Add minimal Chess mock properties/methods if needed */
|
|
26
|
+
} as jest.Mocked<Chess>,
|
|
27
|
+
currentFen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
|
|
28
|
+
currentPosition: "",
|
|
29
|
+
orientation: "w" as Color,
|
|
30
|
+
currentMoveIndex: -1,
|
|
31
|
+
isLatestMove: true,
|
|
32
|
+
info: {
|
|
33
|
+
/* Add minimal GameInfo mock properties/methods if needed */
|
|
34
|
+
} as jest.Mocked<ChessGameContextType["info"]>,
|
|
35
|
+
methods: {
|
|
36
|
+
undo: jest.fn(),
|
|
37
|
+
redo: jest.fn(),
|
|
38
|
+
makeMove: jest.fn(),
|
|
39
|
+
setPosition: jest.fn(),
|
|
40
|
+
flipBoard: jest.fn(),
|
|
41
|
+
goToMove: jest.fn(),
|
|
42
|
+
goToStart: jest.fn(),
|
|
43
|
+
goToEnd: jest.fn(),
|
|
44
|
+
goToPreviousMove: jest.fn(),
|
|
45
|
+
goToNextMove: jest.fn(),
|
|
46
|
+
},
|
|
47
|
+
} as unknown as jest.Mocked<ChessGameContextType>;
|
|
48
|
+
|
|
49
|
+
mockUseChessGameContext.mockReturnValue(mockGameContext);
|
|
50
|
+
|
|
51
|
+
addEventListenerSpy = jest.spyOn(window, "addEventListener");
|
|
52
|
+
removeEventListenerSpy = jest.spyOn(window, "removeEventListener");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
// Restore original implementations
|
|
57
|
+
addEventListenerSpy.mockRestore();
|
|
58
|
+
removeEventListenerSpy.mockRestore();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should add keydown event listener on mount and remove on unmount", () => {
|
|
62
|
+
const { unmount } = renderHook(() => useKeyboardControls());
|
|
63
|
+
expect(addEventListenerSpy).toHaveBeenCalledWith(
|
|
64
|
+
"keydown",
|
|
65
|
+
expect.any(Function),
|
|
66
|
+
);
|
|
67
|
+
expect(removeEventListenerSpy).not.toHaveBeenCalled();
|
|
68
|
+
|
|
69
|
+
unmount();
|
|
70
|
+
expect(removeEventListenerSpy).toHaveBeenCalledWith(
|
|
71
|
+
"keydown",
|
|
72
|
+
expect.any(Function),
|
|
73
|
+
);
|
|
74
|
+
// Ensure the listener functions are the same instance
|
|
75
|
+
expect(addEventListenerSpy.mock.calls[0][1]).toBe(
|
|
76
|
+
removeEventListenerSpy.mock.calls[0][1],
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should call the default handler for a default key", () => {
|
|
81
|
+
renderHook(() => useKeyboardControls());
|
|
82
|
+
const event = new KeyboardEvent("keydown", { key: "ArrowLeft" });
|
|
83
|
+
const preventDefaultSpy = jest.spyOn(event, "preventDefault");
|
|
84
|
+
|
|
85
|
+
act(() => {
|
|
86
|
+
window.dispatchEvent(event);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const undoHandler = mockGameContext.methods.goToPreviousMove;
|
|
90
|
+
|
|
91
|
+
expect(undoHandler).toHaveBeenCalledTimes(1);
|
|
92
|
+
|
|
93
|
+
expect(preventDefaultSpy).toHaveBeenCalledTimes(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should call the custom handler if provided", () => {
|
|
97
|
+
const customHandler = jest.fn();
|
|
98
|
+
const customControls = { z: customHandler };
|
|
99
|
+
|
|
100
|
+
renderHook(() => useKeyboardControls(customControls));
|
|
101
|
+
|
|
102
|
+
const event = new KeyboardEvent("keydown", { key: "z" });
|
|
103
|
+
const preventDefaultSpy = jest.spyOn(event, "preventDefault");
|
|
104
|
+
|
|
105
|
+
act(() => {
|
|
106
|
+
window.dispatchEvent(event);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(customHandler).toHaveBeenCalledWith(mockGameContext);
|
|
110
|
+
expect(customHandler).toHaveBeenCalledTimes(1);
|
|
111
|
+
expect(preventDefaultSpy).toHaveBeenCalledTimes(1);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should call the default handler if a custom handler for a different key is provided", () => {
|
|
115
|
+
const customHandler = jest.fn();
|
|
116
|
+
const customControls = { z: customHandler };
|
|
117
|
+
|
|
118
|
+
renderHook(() => useKeyboardControls(customControls));
|
|
119
|
+
|
|
120
|
+
const event = new KeyboardEvent("keydown", { key: "ArrowRight" });
|
|
121
|
+
const preventDefaultSpy = jest.spyOn(event, "preventDefault");
|
|
122
|
+
|
|
123
|
+
const redoHandler = mockGameContext.methods.goToNextMove;
|
|
124
|
+
|
|
125
|
+
act(() => {
|
|
126
|
+
window.dispatchEvent(event);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(redoHandler).toHaveBeenCalledTimes(1);
|
|
130
|
+
|
|
131
|
+
expect(customHandler).not.toHaveBeenCalled();
|
|
132
|
+
expect(preventDefaultSpy).toHaveBeenCalledTimes(1);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should override the default handler if a custom handler for the same key is provided", () => {
|
|
136
|
+
const customArrowLeftHandler = jest.fn();
|
|
137
|
+
const customControls = { ArrowLeft: customArrowLeftHandler };
|
|
138
|
+
|
|
139
|
+
renderHook(() => useKeyboardControls(customControls));
|
|
140
|
+
|
|
141
|
+
const event = new KeyboardEvent("keydown", { key: "ArrowLeft" });
|
|
142
|
+
const preventDefaultSpy = jest.spyOn(event, "preventDefault");
|
|
143
|
+
|
|
144
|
+
act(() => {
|
|
145
|
+
window.dispatchEvent(event);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(customArrowLeftHandler).toHaveBeenCalledWith(mockGameContext);
|
|
149
|
+
expect(customArrowLeftHandler).toHaveBeenCalledTimes(1);
|
|
150
|
+
expect(mockGameContext.methods.goToPreviousMove).not.toHaveBeenCalled();
|
|
151
|
+
expect(preventDefaultSpy).toHaveBeenCalledTimes(1);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should do nothing if an unmapped key is pressed", () => {
|
|
155
|
+
renderHook(() => useKeyboardControls());
|
|
156
|
+
const event = new KeyboardEvent("keydown", { key: "UnmappedKey" });
|
|
157
|
+
const preventDefaultSpy = jest.spyOn(event, "preventDefault");
|
|
158
|
+
|
|
159
|
+
act(() => {
|
|
160
|
+
window.dispatchEvent(event);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Check that no context functions were called
|
|
164
|
+
Object.values(mockGameContext.methods).forEach((mockFn) => {
|
|
165
|
+
if (jest.isMockFunction(mockFn)) {
|
|
166
|
+
expect(mockFn).not.toHaveBeenCalled();
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
expect(preventDefaultSpy).not.toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
// Components
|
|
1
2
|
export { ChessGame } from "./components/ChessGame";
|
|
3
|
+
|
|
4
|
+
// Hooks & Context
|
|
2
5
|
export { useChessGameContext } from "./hooks/useChessGameContext";
|
|
3
6
|
export { useChessGame } from "./hooks/useChessGame";
|
|
7
|
+
export type { ChessGameContextType } from "./hooks/useChessGameContext";
|
|
8
|
+
export type { useChessGameProps } from "./hooks/useChessGame";
|
|
9
|
+
|
|
10
|
+
// Audio Types
|
|
11
|
+
export type { Sound, Sounds } from "./assets/sounds";
|
|
12
|
+
export type { SoundsProps } from "./components/ChessGame/parts/Sounds";
|
|
13
|
+
|
|
14
|
+
// Keyboard Types
|
|
15
|
+
export type { KeyboardControls } from "./components/ChessGame/parts/KeyboardControls";
|
|
16
|
+
|
|
17
|
+
// Utility Types
|
|
18
|
+
export type { GameInfo } from "./utils/chess";
|
|
19
|
+
|
|
20
|
+
// Component Props
|
|
21
|
+
export type { ChessGameProps } from "./components/ChessGame/parts/Board";
|
|
22
|
+
export type { RootProps } from "./components/ChessGame/parts/Root";
|