@react-chess-tools/react-chess-clock 1.0.1
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 +7 -0
- package/README.md +697 -0
- package/dist/index.cjs +1014 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +528 -0
- package/dist/index.d.ts +528 -0
- package/dist/index.js +969 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
- package/src/components/ChessClock/ChessClock.stories.tsx +782 -0
- package/src/components/ChessClock/index.ts +44 -0
- package/src/components/ChessClock/parts/Display.tsx +69 -0
- package/src/components/ChessClock/parts/PlayPause.tsx +190 -0
- package/src/components/ChessClock/parts/Reset.tsx +90 -0
- package/src/components/ChessClock/parts/Root.tsx +37 -0
- package/src/components/ChessClock/parts/Switch.tsx +84 -0
- package/src/components/ChessClock/parts/__tests__/Display.test.tsx +149 -0
- package/src/components/ChessClock/parts/__tests__/PlayPause.test.tsx +411 -0
- package/src/components/ChessClock/parts/__tests__/Reset.test.tsx +160 -0
- package/src/components/ChessClock/parts/__tests__/Root.test.tsx +49 -0
- package/src/components/ChessClock/parts/__tests__/Switch.test.tsx +204 -0
- package/src/hooks/__tests__/clockReducer.test.ts +985 -0
- package/src/hooks/__tests__/useChessClock.test.tsx +1080 -0
- package/src/hooks/clockReducer.ts +379 -0
- package/src/hooks/useChessClock.ts +406 -0
- package/src/hooks/useChessClockContext.ts +35 -0
- package/src/index.ts +65 -0
- package/src/types.ts +217 -0
- package/src/utils/__tests__/calculateSwitchTime.test.ts +150 -0
- package/src/utils/__tests__/formatTime.test.ts +83 -0
- package/src/utils/__tests__/timeControl.test.ts +414 -0
- package/src/utils/__tests__/timingMethods.test.ts +170 -0
- package/src/utils/calculateSwitchTime.ts +37 -0
- package/src/utils/formatTime.ts +59 -0
- package/src/utils/presets.ts +47 -0
- package/src/utils/timeControl.ts +205 -0
- package/src/utils/timingMethods.ts +103 -0
|
@@ -0,0 +1,1080 @@
|
|
|
1
|
+
import { renderHook, act } from "@testing-library/react";
|
|
2
|
+
import { useChessClock, useOptionalChessClock } from "../useChessClock";
|
|
3
|
+
|
|
4
|
+
// Mock requestAnimationFrame
|
|
5
|
+
const mockRaf = jest.fn();
|
|
6
|
+
const mockCancelRaf = jest.fn();
|
|
7
|
+
|
|
8
|
+
global.requestAnimationFrame =
|
|
9
|
+
mockRaf as unknown as typeof requestAnimationFrame;
|
|
10
|
+
global.cancelAnimationFrame =
|
|
11
|
+
mockCancelRaf as unknown as typeof cancelAnimationFrame;
|
|
12
|
+
|
|
13
|
+
// Mock Date.now() for deterministic timing
|
|
14
|
+
let mockNow = 1_000_000;
|
|
15
|
+
|
|
16
|
+
describe("useChessClock", () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
jest.useFakeTimers();
|
|
19
|
+
mockNow = 1_000_000;
|
|
20
|
+
jest.spyOn(Date, "now").mockImplementation(() => mockNow);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
jest.useRealTimers();
|
|
25
|
+
jest.restoreAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("initialization", () => {
|
|
29
|
+
it("should initialize with correct time from string", () => {
|
|
30
|
+
const { result } = renderHook(() => useChessClock({ time: "5+3" }));
|
|
31
|
+
|
|
32
|
+
expect(result.current.times.white).toBe(300_000); // 5 minutes
|
|
33
|
+
expect(result.current.times.black).toBe(300_000);
|
|
34
|
+
expect(result.current.status).toBe("delayed"); // default clockStart is "delayed"
|
|
35
|
+
expect(result.current.activePlayer).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should initialize with correct time from object", () => {
|
|
39
|
+
const { result } = renderHook(() =>
|
|
40
|
+
useChessClock({
|
|
41
|
+
time: { baseTime: 600, increment: 5 },
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
expect(result.current.times.white).toBe(600_000); // 10 minutes
|
|
46
|
+
expect(result.current.times.black).toBe(600_000);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should initialize with time odds", () => {
|
|
50
|
+
const { result } = renderHook(() =>
|
|
51
|
+
useChessClock({
|
|
52
|
+
time: "5+0",
|
|
53
|
+
whiteTime: 300, // 5 minutes
|
|
54
|
+
blackTime: 180, // 3 minutes
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(result.current.times.white).toBe(300_000);
|
|
59
|
+
expect(result.current.times.black).toBe(180_000);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should start immediately for immediate clock start", () => {
|
|
63
|
+
const { result } = renderHook(() =>
|
|
64
|
+
useChessClock({
|
|
65
|
+
time: "5+0",
|
|
66
|
+
clockStart: "immediate",
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(result.current.status).toBe("running");
|
|
71
|
+
expect(result.current.activePlayer).toBe("white");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should be delayed for delayed clock start", () => {
|
|
75
|
+
const { result } = renderHook(() =>
|
|
76
|
+
useChessClock({
|
|
77
|
+
time: "5+0",
|
|
78
|
+
clockStart: "delayed",
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(result.current.status).toBe("delayed");
|
|
83
|
+
expect(result.current.activePlayer).toBeNull();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should be idle for manual clock start", () => {
|
|
87
|
+
const { result } = renderHook(() =>
|
|
88
|
+
useChessClock({
|
|
89
|
+
time: "5+0",
|
|
90
|
+
clockStart: "manual",
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(result.current.status).toBe("idle");
|
|
95
|
+
expect(result.current.activePlayer).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("info", () => {
|
|
100
|
+
it("should calculate correct info", () => {
|
|
101
|
+
const { result } = renderHook(() => useChessClock({ time: "5+0" }));
|
|
102
|
+
|
|
103
|
+
expect(result.current.info).toEqual({
|
|
104
|
+
isRunning: false,
|
|
105
|
+
isPaused: false,
|
|
106
|
+
isFinished: false,
|
|
107
|
+
isWhiteActive: false,
|
|
108
|
+
isBlackActive: false,
|
|
109
|
+
hasTimeout: false,
|
|
110
|
+
hasTimeOdds: false,
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should detect time odds", () => {
|
|
115
|
+
const { result } = renderHook(() =>
|
|
116
|
+
useChessClock({
|
|
117
|
+
time: "5+0",
|
|
118
|
+
whiteTime: 300,
|
|
119
|
+
blackTime: 180,
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
expect(result.current.info.hasTimeOdds).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("methods.start", () => {
|
|
128
|
+
it("should start the clock from manual mode", () => {
|
|
129
|
+
const { result } = renderHook(() =>
|
|
130
|
+
useChessClock({
|
|
131
|
+
time: "5+0",
|
|
132
|
+
clockStart: "manual",
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
act(() => {
|
|
137
|
+
result.current.methods.start();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(result.current.status).toBe("running");
|
|
141
|
+
expect(result.current.activePlayer).toBe("white");
|
|
142
|
+
expect(result.current.info.isRunning).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should not start countdown in delayed mode until after black's first move", () => {
|
|
146
|
+
const { result } = renderHook(() =>
|
|
147
|
+
useChessClock({
|
|
148
|
+
time: "5+0",
|
|
149
|
+
clockStart: "delayed",
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
expect(result.current.status).toBe("delayed");
|
|
154
|
+
|
|
155
|
+
act(() => {
|
|
156
|
+
result.current.methods.start();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// In delayed mode, START doesn't change the status
|
|
160
|
+
expect(result.current.status).toBe("delayed");
|
|
161
|
+
expect(result.current.activePlayer).toBeNull();
|
|
162
|
+
expect(result.current.info.isRunning).toBe(false);
|
|
163
|
+
|
|
164
|
+
// First switch (white moves)
|
|
165
|
+
act(() => {
|
|
166
|
+
result.current.methods.switch();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Still delayed, black is now active
|
|
170
|
+
expect(result.current.status).toBe("delayed");
|
|
171
|
+
expect(result.current.activePlayer).toBe("black");
|
|
172
|
+
|
|
173
|
+
// Second switch (black moves) - clock starts running
|
|
174
|
+
act(() => {
|
|
175
|
+
result.current.methods.switch();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Now the clock is running
|
|
179
|
+
expect(result.current.status).toBe("running");
|
|
180
|
+
expect(result.current.activePlayer).toBe("white");
|
|
181
|
+
expect(result.current.info.isRunning).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should set active player to white if null", () => {
|
|
185
|
+
const { result } = renderHook(() =>
|
|
186
|
+
useChessClock({
|
|
187
|
+
time: "5+0",
|
|
188
|
+
clockStart: "manual",
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
act(() => {
|
|
193
|
+
result.current.methods.start();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(result.current.activePlayer).toBe("white");
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe("methods.pause", () => {
|
|
201
|
+
it("should pause the clock", () => {
|
|
202
|
+
const { result } = renderHook(() =>
|
|
203
|
+
useChessClock({
|
|
204
|
+
time: "5+0",
|
|
205
|
+
clockStart: "immediate",
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
expect(result.current.status).toBe("running");
|
|
210
|
+
|
|
211
|
+
act(() => {
|
|
212
|
+
result.current.methods.pause();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(result.current.status).toBe("paused");
|
|
216
|
+
expect(result.current.info.isPaused).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("should not pause if not running", () => {
|
|
220
|
+
const { result } = renderHook(() =>
|
|
221
|
+
useChessClock({ time: "5+0", clockStart: "manual" }),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
act(() => {
|
|
225
|
+
result.current.methods.pause();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(result.current.status).toBe("idle");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should maintain correct time when paused", () => {
|
|
232
|
+
const { result } = renderHook(() =>
|
|
233
|
+
useChessClock({
|
|
234
|
+
time: "5+0",
|
|
235
|
+
clockStart: "immediate",
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const initialTime = result.current.times.white;
|
|
240
|
+
expect(initialTime).toBe(300_000);
|
|
241
|
+
|
|
242
|
+
// Advance time by 150ms
|
|
243
|
+
mockNow += 150;
|
|
244
|
+
act(() => {
|
|
245
|
+
jest.advanceTimersByTime(150);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
act(() => {
|
|
249
|
+
result.current.methods.pause();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const pausedTime = result.current.times.white;
|
|
253
|
+
// Time should have decreased by exactly 150ms
|
|
254
|
+
expect(pausedTime).toBe(300_000 - 150);
|
|
255
|
+
|
|
256
|
+
// Resume and check time continues from paused time (not from move start)
|
|
257
|
+
act(() => {
|
|
258
|
+
result.current.methods.resume();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const resumedTime = result.current.times.white;
|
|
262
|
+
// Time should be the same as paused time (no additional time passed)
|
|
263
|
+
expect(resumedTime).toBe(pausedTime);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe("methods.resume", () => {
|
|
268
|
+
it("should resume paused clock", () => {
|
|
269
|
+
const { result } = renderHook(() =>
|
|
270
|
+
useChessClock({
|
|
271
|
+
time: "5+0",
|
|
272
|
+
clockStart: "immediate",
|
|
273
|
+
}),
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
act(() => {
|
|
277
|
+
result.current.methods.pause();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
expect(result.current.status).toBe("paused");
|
|
281
|
+
|
|
282
|
+
act(() => {
|
|
283
|
+
result.current.methods.resume();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(result.current.status).toBe("running");
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("should not resume if not paused", () => {
|
|
290
|
+
const { result } = renderHook(() =>
|
|
291
|
+
useChessClock({ time: "5+0", clockStart: "manual" }),
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
act(() => {
|
|
295
|
+
result.current.methods.resume();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
expect(result.current.status).toBe("idle");
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe("methods.switch", () => {
|
|
303
|
+
it("should switch active player", () => {
|
|
304
|
+
const { result } = renderHook(() =>
|
|
305
|
+
useChessClock({
|
|
306
|
+
time: "5+0",
|
|
307
|
+
clockStart: "immediate",
|
|
308
|
+
}),
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
expect(result.current.activePlayer).toBe("white");
|
|
312
|
+
|
|
313
|
+
act(() => {
|
|
314
|
+
result.current.methods.switch();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
expect(result.current.activePlayer).toBe("black");
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("should switch from delayed to running after two switches", () => {
|
|
321
|
+
const { result } = renderHook(() =>
|
|
322
|
+
useChessClock({
|
|
323
|
+
time: "5+0",
|
|
324
|
+
clockStart: "delayed",
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
expect(result.current.status).toBe("delayed");
|
|
329
|
+
|
|
330
|
+
act(() => {
|
|
331
|
+
result.current.methods.switch();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// After first switch, still in delayed mode, black is active
|
|
335
|
+
expect(result.current.status).toBe("delayed");
|
|
336
|
+
expect(result.current.activePlayer).toBe("black");
|
|
337
|
+
|
|
338
|
+
// Advance time past debounce period
|
|
339
|
+
mockNow += 150;
|
|
340
|
+
act(() => {
|
|
341
|
+
jest.advanceTimersByTime(150);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
act(() => {
|
|
345
|
+
result.current.methods.switch();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// After second switch, clock starts running, white is active
|
|
349
|
+
expect(result.current.status).toBe("running");
|
|
350
|
+
expect(result.current.activePlayer).toBe("white");
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("should apply Fischer increment", () => {
|
|
354
|
+
const onSwitch = jest.fn();
|
|
355
|
+
const { result } = renderHook(() =>
|
|
356
|
+
useChessClock({
|
|
357
|
+
time: "5+3",
|
|
358
|
+
timingMethod: "fischer",
|
|
359
|
+
clockStart: "immediate",
|
|
360
|
+
onSwitch,
|
|
361
|
+
}),
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
const initialWhiteTime = result.current.times.white;
|
|
365
|
+
expect(initialWhiteTime).toBe(300_000);
|
|
366
|
+
|
|
367
|
+
act(() => {
|
|
368
|
+
result.current.methods.switch();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// No real time elapsed (mockNow unchanged), so white gets full increment
|
|
372
|
+
// 300_000 - 0 (time spent) + 3_000 (increment) = 303_000
|
|
373
|
+
expect(result.current.times.white).toBe(303_000);
|
|
374
|
+
expect(onSwitch).toHaveBeenCalledWith("black");
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe("methods.reset", () => {
|
|
379
|
+
it("should reset to initial times", () => {
|
|
380
|
+
const { result } = renderHook(() =>
|
|
381
|
+
useChessClock({
|
|
382
|
+
time: "5+0",
|
|
383
|
+
clockStart: "immediate",
|
|
384
|
+
}),
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
// Manually set a different time (simulating time passing)
|
|
388
|
+
act(() => {
|
|
389
|
+
result.current.methods.setTime("white", 250_000);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
expect(result.current.times.white).toBe(250_000);
|
|
393
|
+
|
|
394
|
+
act(() => {
|
|
395
|
+
result.current.methods.reset();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
expect(result.current.times.white).toBe(300_000);
|
|
399
|
+
expect(result.current.times.black).toBe(300_000);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("should reset to new time control", () => {
|
|
403
|
+
const { result } = renderHook(() => useChessClock({ time: "5+0" }));
|
|
404
|
+
|
|
405
|
+
act(() => {
|
|
406
|
+
result.current.methods.reset("10+5");
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
expect(result.current.times.white).toBe(600_000); // 10 minutes
|
|
410
|
+
expect(result.current.times.black).toBe(600_000);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("should reset status and active player", () => {
|
|
414
|
+
const { result } = renderHook(() =>
|
|
415
|
+
useChessClock({
|
|
416
|
+
time: "5+0",
|
|
417
|
+
clockStart: "immediate",
|
|
418
|
+
}),
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
act(() => {
|
|
422
|
+
result.current.methods.pause();
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
expect(result.current.status).toBe("paused");
|
|
426
|
+
|
|
427
|
+
act(() => {
|
|
428
|
+
result.current.methods.reset();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Reset restores to initial clockStart mode (immediate = running)
|
|
432
|
+
expect(result.current.status).toBe("running");
|
|
433
|
+
expect(result.current.activePlayer).toBe("white");
|
|
434
|
+
expect(result.current.timeout).toBeNull();
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
describe("methods.addTime", () => {
|
|
439
|
+
it("should add time to a player", () => {
|
|
440
|
+
const { result } = renderHook(() => useChessClock({ time: "5+0" }));
|
|
441
|
+
|
|
442
|
+
act(() => {
|
|
443
|
+
result.current.methods.addTime("white", 30_000); // Add 30 seconds
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
expect(result.current.times.white).toBe(330_000);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("should add time to black player", () => {
|
|
450
|
+
const { result } = renderHook(() => useChessClock({ time: "5+0" }));
|
|
451
|
+
|
|
452
|
+
act(() => {
|
|
453
|
+
result.current.methods.addTime("black", 60_000); // Add 1 minute
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
expect(result.current.times.black).toBe(360_000);
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
describe("methods.setTime", () => {
|
|
461
|
+
it("should set time for a player", () => {
|
|
462
|
+
const { result } = renderHook(() => useChessClock({ time: "5+0" }));
|
|
463
|
+
|
|
464
|
+
act(() => {
|
|
465
|
+
result.current.methods.setTime("white", 120_000);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
expect(result.current.times.white).toBe(120_000);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it("should clamp negative time to zero", () => {
|
|
472
|
+
const { result } = renderHook(() => useChessClock({ time: "5+0" }));
|
|
473
|
+
|
|
474
|
+
act(() => {
|
|
475
|
+
result.current.methods.setTime("white", -10_000);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
expect(result.current.times.white).toBe(0);
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
describe("callbacks", () => {
|
|
483
|
+
it("should call onSwitch when player switches", () => {
|
|
484
|
+
const onSwitch = jest.fn();
|
|
485
|
+
const { result } = renderHook(() =>
|
|
486
|
+
useChessClock({
|
|
487
|
+
time: "5+0",
|
|
488
|
+
clockStart: "immediate",
|
|
489
|
+
onSwitch,
|
|
490
|
+
}),
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
act(() => {
|
|
494
|
+
result.current.methods.switch();
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
expect(onSwitch).toHaveBeenCalledWith("black");
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
describe("debounce", () => {
|
|
502
|
+
it("should prevent rapid switches within debounce window", () => {
|
|
503
|
+
const onSwitch = jest.fn();
|
|
504
|
+
const { result } = renderHook(() =>
|
|
505
|
+
useChessClock({
|
|
506
|
+
time: "5+0",
|
|
507
|
+
clockStart: "immediate",
|
|
508
|
+
onSwitch,
|
|
509
|
+
}),
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
// First switch should work
|
|
513
|
+
act(() => {
|
|
514
|
+
result.current.methods.switch();
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
expect(result.current.activePlayer).toBe("black");
|
|
518
|
+
expect(onSwitch).toHaveBeenCalledTimes(1);
|
|
519
|
+
|
|
520
|
+
// Immediate switch should be prevented (debounce window: 100ms)
|
|
521
|
+
expect(result.current.activePlayer).not.toBe("white");
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
describe("options changes", () => {
|
|
526
|
+
it("should auto-reset when time control changes", () => {
|
|
527
|
+
const { result, rerender } = renderHook(
|
|
528
|
+
({ time }) => useChessClock({ time }),
|
|
529
|
+
{ initialProps: { time: "5+3" as string } },
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
expect(result.current.times.white).toBe(300_000);
|
|
533
|
+
|
|
534
|
+
rerender({ time: "10+5" });
|
|
535
|
+
|
|
536
|
+
expect(result.current.times.white).toBe(600_000);
|
|
537
|
+
expect(result.current.times.black).toBe(600_000);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it("should auto-reset when timingMethod changes", () => {
|
|
541
|
+
const { result, rerender } = renderHook(
|
|
542
|
+
({ timingMethod }) => useChessClock({ time: "5+3", timingMethod }),
|
|
543
|
+
{ initialProps: { timingMethod: "fischer" as string } },
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
expect(result.current.timingMethod).toBe("fischer");
|
|
547
|
+
|
|
548
|
+
rerender({ timingMethod: "delay" });
|
|
549
|
+
|
|
550
|
+
expect(result.current.timingMethod).toBe("delay");
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it("should auto-reset when clockStart changes", () => {
|
|
554
|
+
const { result, rerender } = renderHook(
|
|
555
|
+
({ clockStart }) => useChessClock({ time: "5+0", clockStart }),
|
|
556
|
+
{ initialProps: { clockStart: "delayed" as string } },
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
expect(result.current.status).toBe("delayed");
|
|
560
|
+
|
|
561
|
+
rerender({ clockStart: "immediate" });
|
|
562
|
+
|
|
563
|
+
expect(result.current.status).toBe("running");
|
|
564
|
+
expect(result.current.activePlayer).toBe("white");
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it("should auto-reset when whiteTime/blackTime changes (time odds)", () => {
|
|
568
|
+
const { result, rerender } = renderHook(
|
|
569
|
+
({ whiteTime, blackTime }) =>
|
|
570
|
+
useChessClock({ time: "5+0", whiteTime, blackTime }),
|
|
571
|
+
{ initialProps: { whiteTime: 300, blackTime: 300 } },
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
expect(result.current.times.white).toBe(300_000);
|
|
575
|
+
expect(result.current.times.black).toBe(300_000);
|
|
576
|
+
|
|
577
|
+
rerender({ whiteTime: 180, blackTime: 300 });
|
|
578
|
+
|
|
579
|
+
expect(result.current.times.white).toBe(180_000);
|
|
580
|
+
expect(result.current.times.black).toBe(300_000);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it("should NOT reset when only callbacks change", () => {
|
|
584
|
+
const onTimeout1 = jest.fn();
|
|
585
|
+
const onTimeout2 = jest.fn();
|
|
586
|
+
|
|
587
|
+
const { result, rerender } = renderHook(
|
|
588
|
+
({ onTimeout }) => useChessClock({ time: "5+3", onTimeout }),
|
|
589
|
+
{ initialProps: { onTimeout: onTimeout1 } },
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
const initialTimes = result.current.times;
|
|
593
|
+
|
|
594
|
+
rerender({ onTimeout: onTimeout2 });
|
|
595
|
+
|
|
596
|
+
// Times should be unchanged
|
|
597
|
+
expect(result.current.times).toEqual(initialTimes);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it("should NOT reset when onSwitch callback changes", () => {
|
|
601
|
+
const onSwitch1 = jest.fn();
|
|
602
|
+
const onSwitch2 = jest.fn();
|
|
603
|
+
|
|
604
|
+
const { result, rerender } = renderHook(
|
|
605
|
+
({ onSwitch }) =>
|
|
606
|
+
useChessClock({ time: "5+3", clockStart: "immediate", onSwitch }),
|
|
607
|
+
{ initialProps: { onSwitch: onSwitch1 } },
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
const initialTimes = result.current.times;
|
|
611
|
+
|
|
612
|
+
rerender({ onSwitch: onSwitch2 });
|
|
613
|
+
|
|
614
|
+
// Times should be unchanged (with mocked Date.now, no real time passes)
|
|
615
|
+
expect(result.current.times).toEqual(initialTimes);
|
|
616
|
+
|
|
617
|
+
// New callback should be used
|
|
618
|
+
act(() => {
|
|
619
|
+
result.current.methods.switch();
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
expect(onSwitch2).toHaveBeenCalledWith("black");
|
|
623
|
+
expect(onSwitch1).not.toHaveBeenCalled();
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it("should NOT reset when onTimeUpdate callback changes", () => {
|
|
627
|
+
const onTimeUpdate1 = jest.fn();
|
|
628
|
+
const onTimeUpdate2 = jest.fn();
|
|
629
|
+
|
|
630
|
+
const { result, rerender } = renderHook(
|
|
631
|
+
({ onTimeUpdate }) => useChessClock({ time: "5+3", onTimeUpdate }),
|
|
632
|
+
{ initialProps: { onTimeUpdate: onTimeUpdate1 } },
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
const initialTimes = result.current.times;
|
|
636
|
+
|
|
637
|
+
rerender({ onTimeUpdate: onTimeUpdate2 });
|
|
638
|
+
|
|
639
|
+
// Times should be unchanged
|
|
640
|
+
expect(result.current.times).toEqual(initialTimes);
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it("should reset status when time control changes during game", () => {
|
|
644
|
+
const { result, rerender } = renderHook(
|
|
645
|
+
({ time }) => useChessClock({ time, clockStart: "immediate" }),
|
|
646
|
+
{ initialProps: { time: "5+3" as string } },
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
// Start the clock and make a switch
|
|
650
|
+
act(() => {
|
|
651
|
+
result.current.methods.switch();
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
expect(result.current.status).toBe("running");
|
|
655
|
+
expect(result.current.activePlayer).toBe("black");
|
|
656
|
+
|
|
657
|
+
// Change time control - should reset to initial state
|
|
658
|
+
rerender({ time: "10+5" });
|
|
659
|
+
|
|
660
|
+
expect(result.current.status).toBe("running"); // immediate mode
|
|
661
|
+
expect(result.current.activePlayer).toBe("white"); // reset to white
|
|
662
|
+
expect(result.current.times.white).toBe(600_000);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it("should handle multiple option changes", () => {
|
|
666
|
+
const { result, rerender } = renderHook(
|
|
667
|
+
({ time, timingMethod }) => useChessClock({ time, timingMethod }),
|
|
668
|
+
{
|
|
669
|
+
initialProps: {
|
|
670
|
+
time: "5+3" as string,
|
|
671
|
+
timingMethod: "fischer" as string,
|
|
672
|
+
},
|
|
673
|
+
},
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
expect(result.current.times.white).toBe(300_000);
|
|
677
|
+
expect(result.current.timingMethod).toBe("fischer");
|
|
678
|
+
|
|
679
|
+
rerender({ time: "10+5", timingMethod: "delay" });
|
|
680
|
+
|
|
681
|
+
expect(result.current.times.white).toBe(600_000);
|
|
682
|
+
expect(result.current.timingMethod).toBe("delay");
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
describe("useOptionalChessClock", () => {
|
|
688
|
+
beforeEach(() => {
|
|
689
|
+
jest.useFakeTimers();
|
|
690
|
+
mockNow = 1_000_000;
|
|
691
|
+
jest.spyOn(Date, "now").mockImplementation(() => mockNow);
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
afterEach(() => {
|
|
695
|
+
jest.useRealTimers();
|
|
696
|
+
jest.restoreAllMocks();
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it("should return null when options is undefined", () => {
|
|
700
|
+
const { result } = renderHook(() => useOptionalChessClock(undefined));
|
|
701
|
+
|
|
702
|
+
expect(result.current).toBeNull();
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it("should return clock state when options is provided", () => {
|
|
706
|
+
const { result } = renderHook(() => useOptionalChessClock({ time: "5+3" }));
|
|
707
|
+
|
|
708
|
+
expect(result.current).not.toBeNull();
|
|
709
|
+
expect(result.current?.times.white).toBe(300_000);
|
|
710
|
+
expect(result.current?.times.black).toBe(300_000);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it("should work the same as useChessClock when enabled", () => {
|
|
714
|
+
const config = { time: "10+5" as const };
|
|
715
|
+
|
|
716
|
+
const { result: optionalResult } = renderHook(() =>
|
|
717
|
+
useOptionalChessClock(config),
|
|
718
|
+
);
|
|
719
|
+
const { result: requiredResult } = renderHook(() => useChessClock(config));
|
|
720
|
+
|
|
721
|
+
expect(optionalResult.current?.times).toEqual(requiredResult.current.times);
|
|
722
|
+
expect(optionalResult.current?.status).toEqual(
|
|
723
|
+
requiredResult.current.status,
|
|
724
|
+
);
|
|
725
|
+
expect(optionalResult.current?.activePlayer).toEqual(
|
|
726
|
+
requiredResult.current.activePlayer,
|
|
727
|
+
);
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
describe("useChessClock - multi-period time controls", () => {
|
|
732
|
+
beforeEach(() => {
|
|
733
|
+
jest.useFakeTimers();
|
|
734
|
+
mockNow = 1_000_000;
|
|
735
|
+
jest.spyOn(Date, "now").mockImplementation(() => mockNow);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
afterEach(() => {
|
|
739
|
+
jest.useRealTimers();
|
|
740
|
+
jest.restoreAllMocks();
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
describe("initialization", () => {
|
|
744
|
+
it("should initialize with multi-period state from array config", () => {
|
|
745
|
+
const { result } = renderHook(() =>
|
|
746
|
+
useChessClock({
|
|
747
|
+
time: [
|
|
748
|
+
{ baseTime: 5400, increment: 30, moves: 40 },
|
|
749
|
+
{ baseTime: 1800, increment: 30 },
|
|
750
|
+
],
|
|
751
|
+
}),
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
expect(result.current.totalPeriods).toBe(2);
|
|
755
|
+
expect(result.current.currentPeriodIndex).toEqual({ white: 0, black: 0 });
|
|
756
|
+
expect(result.current.periodMoves).toEqual({ white: 0, black: 0 });
|
|
757
|
+
expect(result.current.currentPeriod.white).toEqual({
|
|
758
|
+
baseTime: 5400,
|
|
759
|
+
increment: 30,
|
|
760
|
+
moves: 40,
|
|
761
|
+
});
|
|
762
|
+
expect(result.current.currentPeriod.black).toEqual({
|
|
763
|
+
baseTime: 5400,
|
|
764
|
+
increment: 30,
|
|
765
|
+
moves: 40,
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
it("should expose total periods correctly", () => {
|
|
770
|
+
const { result } = renderHook(() =>
|
|
771
|
+
useChessClock({
|
|
772
|
+
time: [
|
|
773
|
+
{ baseTime: 5400, increment: 30, moves: 40 },
|
|
774
|
+
{ baseTime: 3600, increment: 30, moves: 20 },
|
|
775
|
+
{ baseTime: 900, increment: 30 },
|
|
776
|
+
],
|
|
777
|
+
}),
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
expect(result.current.totalPeriods).toBe(3);
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
it("should initialize with single period when not multi-period", () => {
|
|
784
|
+
const { result } = renderHook(() => useChessClock({ time: "5+3" }));
|
|
785
|
+
|
|
786
|
+
expect(result.current.totalPeriods).toBe(1);
|
|
787
|
+
expect(result.current.currentPeriodIndex).toEqual({ white: 0, black: 0 });
|
|
788
|
+
expect(result.current.currentPeriod.white).toMatchObject({
|
|
789
|
+
baseTime: 300,
|
|
790
|
+
increment: 3,
|
|
791
|
+
});
|
|
792
|
+
// Note: delay may be present in the actual object
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
it("should initialize initial times from first period", () => {
|
|
796
|
+
const { result } = renderHook(() =>
|
|
797
|
+
useChessClock({
|
|
798
|
+
time: [
|
|
799
|
+
{ baseTime: 5400, increment: 30, moves: 40 }, // 90 minutes
|
|
800
|
+
{ baseTime: 1800, increment: 30 },
|
|
801
|
+
],
|
|
802
|
+
}),
|
|
803
|
+
);
|
|
804
|
+
|
|
805
|
+
expect(result.current.times.white).toBe(5_400_000); // 90 minutes in ms
|
|
806
|
+
expect(result.current.times.black).toBe(5_400_000);
|
|
807
|
+
});
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
describe("period transitions", () => {
|
|
811
|
+
it("should track period moves for each player", () => {
|
|
812
|
+
const { result } = renderHook(() =>
|
|
813
|
+
useChessClock({
|
|
814
|
+
time: [
|
|
815
|
+
{ baseTime: 300, increment: 5, moves: 3 }, // 5 min, 3 moves to advance
|
|
816
|
+
{ baseTime: 180, increment: 3 },
|
|
817
|
+
],
|
|
818
|
+
clockStart: "immediate",
|
|
819
|
+
}),
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
// Initial state
|
|
823
|
+
expect(result.current.periodMoves).toEqual({ white: 0, black: 0 });
|
|
824
|
+
|
|
825
|
+
// White's first move
|
|
826
|
+
act(() => {
|
|
827
|
+
result.current.methods.switch();
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
expect(result.current.periodMoves).toEqual({ white: 1, black: 0 });
|
|
831
|
+
|
|
832
|
+
// Black's first move
|
|
833
|
+
act(() => {
|
|
834
|
+
result.current.methods.switch();
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
expect(result.current.periodMoves).toEqual({ white: 1, black: 1 });
|
|
838
|
+
|
|
839
|
+
// White's second move
|
|
840
|
+
act(() => {
|
|
841
|
+
result.current.methods.switch();
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
expect(result.current.periodMoves).toEqual({ white: 2, black: 1 });
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it("should advance player to next period after completing required moves", () => {
|
|
848
|
+
const { result } = renderHook(() =>
|
|
849
|
+
useChessClock({
|
|
850
|
+
time: [
|
|
851
|
+
{ baseTime: 300, increment: 5, moves: 2 }, // 2 moves to advance
|
|
852
|
+
{ baseTime: 180, increment: 3 },
|
|
853
|
+
],
|
|
854
|
+
clockStart: "immediate",
|
|
855
|
+
}),
|
|
856
|
+
);
|
|
857
|
+
|
|
858
|
+
const initialWhiteTime = result.current.times.white;
|
|
859
|
+
|
|
860
|
+
// Make moves: white(1), black(1), white(2) -> white advances
|
|
861
|
+
act(() => {
|
|
862
|
+
result.current.methods.switch(); // white moves
|
|
863
|
+
});
|
|
864
|
+
expect(result.current.currentPeriodIndex.white).toBe(0);
|
|
865
|
+
|
|
866
|
+
act(() => {
|
|
867
|
+
result.current.methods.switch(); // black moves
|
|
868
|
+
});
|
|
869
|
+
expect(result.current.currentPeriodIndex.black).toBe(0);
|
|
870
|
+
|
|
871
|
+
act(() => {
|
|
872
|
+
result.current.methods.switch(); // white moves again (2nd move)
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
// White should advance to period 1
|
|
876
|
+
expect(result.current.currentPeriodIndex.white).toBe(1);
|
|
877
|
+
// Black should still be in period 0
|
|
878
|
+
expect(result.current.currentPeriodIndex.black).toBe(0);
|
|
879
|
+
// White's period moves should reset
|
|
880
|
+
expect(result.current.periodMoves.white).toBe(0);
|
|
881
|
+
// White should receive period 1's base time (180s = 180,000ms) plus increments from switches
|
|
882
|
+
expect(result.current.times.white).toBeGreaterThanOrEqual(
|
|
883
|
+
initialWhiteTime + 180_000,
|
|
884
|
+
);
|
|
885
|
+
expect(result.current.times.white).toBeLessThanOrEqual(
|
|
886
|
+
initialWhiteTime + 180_000 + 20_000, // Allow for increment variance
|
|
887
|
+
);
|
|
888
|
+
// White's current period should reflect new settings
|
|
889
|
+
expect(result.current.currentPeriod.white).toMatchObject({
|
|
890
|
+
baseTime: 180,
|
|
891
|
+
increment: 3,
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
it("should handle both players advancing simultaneously", () => {
|
|
896
|
+
const { result } = renderHook(() =>
|
|
897
|
+
useChessClock({
|
|
898
|
+
time: [
|
|
899
|
+
{ baseTime: 300, increment: 5, moves: 2 },
|
|
900
|
+
{ baseTime: 180, increment: 3 },
|
|
901
|
+
],
|
|
902
|
+
clockStart: "immediate",
|
|
903
|
+
}),
|
|
904
|
+
);
|
|
905
|
+
|
|
906
|
+
// Make 2 moves each: both should advance to period 1
|
|
907
|
+
for (let i = 0; i < 4; i++) {
|
|
908
|
+
act(() => {
|
|
909
|
+
result.current.methods.switch();
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
expect(result.current.currentPeriodIndex).toEqual({ white: 1, black: 1 });
|
|
914
|
+
expect(result.current.periodMoves).toEqual({ white: 0, black: 0 });
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
it("should not advance from sudden death period", () => {
|
|
918
|
+
const { result } = renderHook(() =>
|
|
919
|
+
useChessClock({
|
|
920
|
+
time: [
|
|
921
|
+
{ baseTime: 300, increment: 5, moves: 2 },
|
|
922
|
+
{ baseTime: 180, increment: 3 }, // Sudden death
|
|
923
|
+
],
|
|
924
|
+
clockStart: "immediate",
|
|
925
|
+
}),
|
|
926
|
+
);
|
|
927
|
+
|
|
928
|
+
// Advance to period 1
|
|
929
|
+
for (let i = 0; i < 4; i++) {
|
|
930
|
+
act(() => {
|
|
931
|
+
result.current.methods.switch();
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
expect(result.current.currentPeriodIndex).toEqual({ white: 1, black: 1 });
|
|
936
|
+
|
|
937
|
+
const whiteTimeAfterAdvance = result.current.times.white;
|
|
938
|
+
|
|
939
|
+
// Make more moves - should not advance further
|
|
940
|
+
for (let i = 0; i < 4; i++) {
|
|
941
|
+
act(() => {
|
|
942
|
+
result.current.methods.switch();
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Still in period 1
|
|
947
|
+
expect(result.current.currentPeriodIndex).toEqual({ white: 1, black: 1 });
|
|
948
|
+
// Period moves continue to increment
|
|
949
|
+
expect(result.current.periodMoves.white).toBeGreaterThan(0);
|
|
950
|
+
// No additional base time added
|
|
951
|
+
expect(result.current.times.white).not.toBeGreaterThan(
|
|
952
|
+
whiteTimeAfterAdvance + 10_000, // Allow for small timing variation
|
|
953
|
+
);
|
|
954
|
+
});
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
describe("reset with multi-period", () => {
|
|
958
|
+
it("should reset period state on reset", () => {
|
|
959
|
+
const { result } = renderHook(() =>
|
|
960
|
+
useChessClock({
|
|
961
|
+
time: [
|
|
962
|
+
{ baseTime: 300, increment: 5, moves: 2 },
|
|
963
|
+
{ baseTime: 180, increment: 3 },
|
|
964
|
+
],
|
|
965
|
+
clockStart: "immediate",
|
|
966
|
+
}),
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
// Advance to period 1
|
|
970
|
+
for (let i = 0; i < 4; i++) {
|
|
971
|
+
act(() => {
|
|
972
|
+
result.current.methods.switch();
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
expect(result.current.currentPeriodIndex).toEqual({ white: 1, black: 1 });
|
|
977
|
+
|
|
978
|
+
// Reset
|
|
979
|
+
act(() => {
|
|
980
|
+
result.current.methods.reset();
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
// Period state should be reset
|
|
984
|
+
expect(result.current.currentPeriodIndex).toEqual({ white: 0, black: 0 });
|
|
985
|
+
expect(result.current.periodMoves).toEqual({ white: 0, black: 0 });
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
it("should reset to new multi-period time control", () => {
|
|
989
|
+
const { result } = renderHook(() =>
|
|
990
|
+
useChessClock({
|
|
991
|
+
time: [
|
|
992
|
+
{ baseTime: 300, increment: 5, moves: 2 },
|
|
993
|
+
{ baseTime: 180, increment: 3 },
|
|
994
|
+
],
|
|
995
|
+
}),
|
|
996
|
+
);
|
|
997
|
+
|
|
998
|
+
expect(result.current.totalPeriods).toBe(2);
|
|
999
|
+
|
|
1000
|
+
// Reset to single period
|
|
1001
|
+
act(() => {
|
|
1002
|
+
result.current.methods.reset("10+5");
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
expect(result.current.totalPeriods).toBe(1);
|
|
1006
|
+
expect(result.current.currentPeriodIndex).toEqual({ white: 0, black: 0 });
|
|
1007
|
+
});
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
describe("three-period time control", () => {
|
|
1011
|
+
it("should handle three-period tournament time control", () => {
|
|
1012
|
+
const { result } = renderHook(() =>
|
|
1013
|
+
useChessClock({
|
|
1014
|
+
time: [
|
|
1015
|
+
{ baseTime: 5400, increment: 30, moves: 40 }, // Period 1
|
|
1016
|
+
{ baseTime: 3600, increment: 30, moves: 20 }, // Period 2
|
|
1017
|
+
{ baseTime: 900, increment: 30 }, // Period 3 (sudden death)
|
|
1018
|
+
],
|
|
1019
|
+
clockStart: "immediate",
|
|
1020
|
+
}),
|
|
1021
|
+
);
|
|
1022
|
+
|
|
1023
|
+
expect(result.current.totalPeriods).toBe(3);
|
|
1024
|
+
|
|
1025
|
+
// Simulate completing period 1 (40 moves each)
|
|
1026
|
+
// We'll do 4 moves to demonstrate the mechanics
|
|
1027
|
+
const initialWhiteTime = result.current.times.white;
|
|
1028
|
+
|
|
1029
|
+
act(() => {
|
|
1030
|
+
result.current.methods.switch(); // 1
|
|
1031
|
+
});
|
|
1032
|
+
act(() => {
|
|
1033
|
+
result.current.methods.switch(); // 2
|
|
1034
|
+
});
|
|
1035
|
+
act(() => {
|
|
1036
|
+
result.current.methods.switch(); // 3
|
|
1037
|
+
});
|
|
1038
|
+
act(() => {
|
|
1039
|
+
result.current.methods.switch(); // 4
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
// After 4 moves (2 each), still in period 0
|
|
1043
|
+
expect(result.current.currentPeriodIndex.white).toBe(0);
|
|
1044
|
+
expect(result.current.periodMoves.white).toBe(2);
|
|
1045
|
+
// Time should have increased due to increments (30 seconds per white move = 2 moves)
|
|
1046
|
+
expect(result.current.times.white).toBeGreaterThan(initialWhiteTime);
|
|
1047
|
+
expect(result.current.times.white).toBeLessThanOrEqual(
|
|
1048
|
+
initialWhiteTime + 70_000, // Allow some variance
|
|
1049
|
+
);
|
|
1050
|
+
});
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
describe("independent player advancement", () => {
|
|
1054
|
+
it("should handle white advancing while black stays in earlier period", () => {
|
|
1055
|
+
const { result } = renderHook(() =>
|
|
1056
|
+
useChessClock({
|
|
1057
|
+
time: [
|
|
1058
|
+
{ baseTime: 300, increment: 5, moves: 2 },
|
|
1059
|
+
{ baseTime: 180, increment: 3, moves: 2 },
|
|
1060
|
+
{ baseTime: 60, increment: 2 },
|
|
1061
|
+
],
|
|
1062
|
+
clockStart: "immediate",
|
|
1063
|
+
}),
|
|
1064
|
+
);
|
|
1065
|
+
|
|
1066
|
+
// Make moves to demonstrate independent advancement
|
|
1067
|
+
for (let i = 0; i < 8; i++) {
|
|
1068
|
+
act(() => {
|
|
1069
|
+
result.current.methods.switch();
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// After 8 moves (4 each), both players advance twice:
|
|
1074
|
+
expect(result.current.currentPeriodIndex.white).toBe(2);
|
|
1075
|
+
expect(result.current.currentPeriodIndex.black).toBe(2);
|
|
1076
|
+
expect(result.current.periodMoves.white).toBe(0); // Reset after second advancement
|
|
1077
|
+
expect(result.current.periodMoves.black).toBe(0);
|
|
1078
|
+
});
|
|
1079
|
+
});
|
|
1080
|
+
});
|