@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,985 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clockReducer,
|
|
3
|
+
createInitialClockState,
|
|
4
|
+
ClockState,
|
|
5
|
+
} from "../clockReducer";
|
|
6
|
+
import { parseTimeControlConfig } from "../../utils/timeControl";
|
|
7
|
+
import { ClockStatus, PeriodState } from "../../types";
|
|
8
|
+
|
|
9
|
+
describe("clockReducer", () => {
|
|
10
|
+
const initialTimes = { white: 300000, black: 300000 };
|
|
11
|
+
const defaultConfig = parseTimeControlConfig({ time: "5+0" });
|
|
12
|
+
|
|
13
|
+
function createState(overrides?: Partial<ClockState>): ClockState {
|
|
14
|
+
const { moveStartTime, elapsedAtPause, ...rest } = overrides ?? {};
|
|
15
|
+
return {
|
|
16
|
+
times: initialTimes,
|
|
17
|
+
initialTimes,
|
|
18
|
+
status: "idle",
|
|
19
|
+
activePlayer: null,
|
|
20
|
+
timeout: null,
|
|
21
|
+
switchCount: 0,
|
|
22
|
+
config: defaultConfig,
|
|
23
|
+
moveStartTime: moveStartTime ?? null,
|
|
24
|
+
elapsedAtPause: elapsedAtPause ?? 0,
|
|
25
|
+
...rest,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("START", () => {
|
|
30
|
+
it("should start the clock and set active player to white", () => {
|
|
31
|
+
const state = createState();
|
|
32
|
+
const result = clockReducer(state, { type: "START" });
|
|
33
|
+
|
|
34
|
+
expect(result.status).toBe("running");
|
|
35
|
+
expect(result.activePlayer).toBe("white");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should preserve existing active player", () => {
|
|
39
|
+
const state = createState({ activePlayer: "black" });
|
|
40
|
+
const result = clockReducer(state, { type: "START" });
|
|
41
|
+
|
|
42
|
+
expect(result.status).toBe("running");
|
|
43
|
+
expect(result.activePlayer).toBe("black");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should not start if already finished", () => {
|
|
47
|
+
const state = createState({ status: "finished" });
|
|
48
|
+
const result = clockReducer(state, { type: "START" });
|
|
49
|
+
|
|
50
|
+
expect(result).toBe(state);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("PAUSE", () => {
|
|
55
|
+
it("should pause a running clock", () => {
|
|
56
|
+
const state = createState({ status: "running", activePlayer: "white" });
|
|
57
|
+
const result = clockReducer(state, { type: "PAUSE" });
|
|
58
|
+
|
|
59
|
+
expect(result.status).toBe("paused");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should not pause if not running", () => {
|
|
63
|
+
const state = createState({ status: "idle" });
|
|
64
|
+
const result = clockReducer(state, { type: "PAUSE" });
|
|
65
|
+
|
|
66
|
+
expect(result).toBe(state);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("RESUME", () => {
|
|
71
|
+
it("should resume a paused clock", () => {
|
|
72
|
+
const state = createState({ status: "paused", activePlayer: "white" });
|
|
73
|
+
const result = clockReducer(state, { type: "RESUME" });
|
|
74
|
+
|
|
75
|
+
expect(result.status).toBe("running");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should not resume if not paused", () => {
|
|
79
|
+
const state = createState({ status: "running", activePlayer: "white" });
|
|
80
|
+
const result = clockReducer(state, { type: "RESUME" });
|
|
81
|
+
|
|
82
|
+
expect(result).toBe(state);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("SWITCH", () => {
|
|
87
|
+
it("should switch to the opposite player", () => {
|
|
88
|
+
const state = createState({ status: "running", activePlayer: "white" });
|
|
89
|
+
const result = clockReducer(state, { type: "SWITCH", payload: {} });
|
|
90
|
+
|
|
91
|
+
expect(result.activePlayer).toBe("black");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should switch from black to white", () => {
|
|
95
|
+
const state = createState({ status: "running", activePlayer: "black" });
|
|
96
|
+
const result = clockReducer(state, { type: "SWITCH", payload: {} });
|
|
97
|
+
|
|
98
|
+
expect(result.activePlayer).toBe("white");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should update times if provided", () => {
|
|
102
|
+
const state = createState({ status: "running", activePlayer: "white" });
|
|
103
|
+
const newTimes = { white: 290000, black: 300000 };
|
|
104
|
+
const result = clockReducer(state, {
|
|
105
|
+
type: "SWITCH",
|
|
106
|
+
payload: { newTimes },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(result.times).toEqual(newTimes);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should start clock if idle but activePlayer is set", () => {
|
|
113
|
+
const state = createState({ status: "idle", activePlayer: "white" });
|
|
114
|
+
const result = clockReducer(state, { type: "SWITCH", payload: {} });
|
|
115
|
+
|
|
116
|
+
expect(result.status).toBe("running");
|
|
117
|
+
expect(result.activePlayer).toBe("black");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should no-op if activePlayer is null", () => {
|
|
121
|
+
const state = createState({ status: "idle", activePlayer: null });
|
|
122
|
+
const result = clockReducer(state, { type: "SWITCH", payload: {} });
|
|
123
|
+
|
|
124
|
+
expect(result).toBe(state);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should preserve times if not provided", () => {
|
|
128
|
+
const state = createState({ status: "running", activePlayer: "white" });
|
|
129
|
+
const result = clockReducer(state, { type: "SWITCH", payload: {} });
|
|
130
|
+
|
|
131
|
+
expect(result.times).toEqual(initialTimes);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should not switch if already finished", () => {
|
|
135
|
+
const state = createState({
|
|
136
|
+
status: "finished",
|
|
137
|
+
activePlayer: "white",
|
|
138
|
+
timeout: "white",
|
|
139
|
+
times: { white: 0, black: 300000 },
|
|
140
|
+
});
|
|
141
|
+
const result = clockReducer(state, { type: "SWITCH", payload: {} });
|
|
142
|
+
|
|
143
|
+
expect(result).toBe(state);
|
|
144
|
+
expect(result.status).toBe("finished");
|
|
145
|
+
expect(result.activePlayer).toBe("white");
|
|
146
|
+
expect(result.switchCount).toBe(0);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("SWITCH (delayed mode)", () => {
|
|
151
|
+
it("should set active player to black on first switch in delayed mode", () => {
|
|
152
|
+
const state = createState({ status: "delayed" });
|
|
153
|
+
const result = clockReducer(state, { type: "SWITCH", payload: {} });
|
|
154
|
+
|
|
155
|
+
expect(result.activePlayer).toBe("black");
|
|
156
|
+
expect(result.switchCount).toBe(1);
|
|
157
|
+
expect(result.status).toBe("delayed");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("should set active player to white on second switch and start clock", () => {
|
|
161
|
+
const state = createState({ status: "delayed", switchCount: 1 });
|
|
162
|
+
const result = clockReducer(state, { type: "SWITCH", payload: {} });
|
|
163
|
+
|
|
164
|
+
expect(result.activePlayer).toBe("white");
|
|
165
|
+
expect(result.switchCount).toBe(2);
|
|
166
|
+
expect(result.status).toBe("running");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should not modify times during delayed switches", () => {
|
|
170
|
+
const state = createState({
|
|
171
|
+
status: "delayed",
|
|
172
|
+
times: { white: 300000, black: 300000 },
|
|
173
|
+
});
|
|
174
|
+
const result = clockReducer(state, { type: "SWITCH", payload: {} });
|
|
175
|
+
|
|
176
|
+
expect(result.times).toEqual({ white: 300000, black: 300000 });
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("TIMEOUT", () => {
|
|
181
|
+
it("should set finished status and timeout player", () => {
|
|
182
|
+
const state = createState({ status: "running", activePlayer: "white" });
|
|
183
|
+
const result = clockReducer(state, {
|
|
184
|
+
type: "TIMEOUT",
|
|
185
|
+
payload: { player: "white" },
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(result.status).toBe("finished");
|
|
189
|
+
expect(result.timeout).toBe("white");
|
|
190
|
+
expect(result.times.white).toBe(0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("should preserve other player time", () => {
|
|
194
|
+
const state = createState({
|
|
195
|
+
status: "running",
|
|
196
|
+
activePlayer: "white",
|
|
197
|
+
times: { white: 100, black: 300000 },
|
|
198
|
+
});
|
|
199
|
+
const result = clockReducer(state, {
|
|
200
|
+
type: "TIMEOUT",
|
|
201
|
+
payload: { player: "white" },
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(result.times.black).toBe(300000);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("RESET", () => {
|
|
209
|
+
it("should reset to new initial state", () => {
|
|
210
|
+
const state = createState({
|
|
211
|
+
status: "finished",
|
|
212
|
+
activePlayer: "black",
|
|
213
|
+
timeout: "white",
|
|
214
|
+
switchCount: 5,
|
|
215
|
+
times: { white: 0, black: 100000 },
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const result = clockReducer(state, {
|
|
219
|
+
type: "RESET",
|
|
220
|
+
payload: { time: "10+0" },
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(result.times).toEqual({ white: 600000, black: 600000 });
|
|
224
|
+
expect(result.initialTimes).toEqual({ white: 600000, black: 600000 });
|
|
225
|
+
expect(result.status).toBe("delayed");
|
|
226
|
+
expect(result.activePlayer).toBeNull();
|
|
227
|
+
expect(result.timeout).toBeNull();
|
|
228
|
+
expect(result.switchCount).toBe(0);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should reset to immediate clock start", () => {
|
|
232
|
+
const state = createState({
|
|
233
|
+
status: "finished",
|
|
234
|
+
activePlayer: "black",
|
|
235
|
+
timeout: "white",
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const result = clockReducer(state, {
|
|
239
|
+
type: "RESET",
|
|
240
|
+
payload: { time: "10+0", clockStart: "immediate" },
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
expect(result.status).toBe("running");
|
|
244
|
+
expect(result.activePlayer).toBe("white");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("should set moveStartTime when resetting to immediate clock start", () => {
|
|
248
|
+
const state = createState({
|
|
249
|
+
status: "finished",
|
|
250
|
+
activePlayer: "black",
|
|
251
|
+
timeout: "white",
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const result = clockReducer(state, {
|
|
255
|
+
type: "RESET",
|
|
256
|
+
payload: { time: "10+0", clockStart: "immediate" },
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// When clock starts immediately, moveStartTime should be set
|
|
260
|
+
// so the clock display can count down correctly
|
|
261
|
+
expect(result.moveStartTime).not.toBeNull();
|
|
262
|
+
expect(typeof result.moveStartTime).toBe("number");
|
|
263
|
+
expect(result.moveStartTime).toBeGreaterThan(0);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("should respect explicit now parameter for purity", () => {
|
|
267
|
+
const state = createState({
|
|
268
|
+
status: "finished",
|
|
269
|
+
activePlayer: "black",
|
|
270
|
+
timeout: "white",
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const now = 1234567890;
|
|
274
|
+
const result = clockReducer(state, {
|
|
275
|
+
type: "RESET",
|
|
276
|
+
payload: { time: "10+0", clockStart: "immediate", now },
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(result.moveStartTime).toBe(now);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("should produce deterministic output with same now parameter", () => {
|
|
283
|
+
const state = createState({
|
|
284
|
+
status: "finished",
|
|
285
|
+
activePlayer: "black",
|
|
286
|
+
timeout: "white",
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const now = 1234567890;
|
|
290
|
+
const result1 = clockReducer(state, {
|
|
291
|
+
type: "RESET",
|
|
292
|
+
payload: { time: "10+0", clockStart: "immediate", now },
|
|
293
|
+
});
|
|
294
|
+
const result2 = clockReducer(state, {
|
|
295
|
+
type: "RESET",
|
|
296
|
+
payload: { time: "10+0", clockStart: "immediate", now },
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// Same inputs should produce same outputs (purity)
|
|
300
|
+
expect(result1.moveStartTime).toBe(result2.moveStartTime);
|
|
301
|
+
expect(result1.moveStartTime).toBe(now);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("should work without now parameter for backward compatibility", () => {
|
|
305
|
+
const state = createState({
|
|
306
|
+
status: "finished",
|
|
307
|
+
activePlayer: "black",
|
|
308
|
+
timeout: "white",
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const result = clockReducer(state, {
|
|
312
|
+
type: "RESET",
|
|
313
|
+
payload: { time: "10+0", clockStart: "immediate" },
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Should still work when now is not provided (uses Date.now() as fallback)
|
|
317
|
+
expect(result.moveStartTime).not.toBeNull();
|
|
318
|
+
expect(typeof result.moveStartTime).toBe("number");
|
|
319
|
+
expect(result.moveStartTime).toBeGreaterThan(0);
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe("ADD_TIME", () => {
|
|
324
|
+
it("should add time to specified player", () => {
|
|
325
|
+
const state = createState();
|
|
326
|
+
const result = clockReducer(state, {
|
|
327
|
+
type: "ADD_TIME",
|
|
328
|
+
payload: { player: "white", milliseconds: 10000 },
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
expect(result.times.white).toBe(310000);
|
|
332
|
+
expect(result.times.black).toBe(300000);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("should handle negative additions", () => {
|
|
336
|
+
const state = createState();
|
|
337
|
+
const result = clockReducer(state, {
|
|
338
|
+
type: "ADD_TIME",
|
|
339
|
+
payload: { player: "black", milliseconds: -50000 },
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(result.times.black).toBe(250000);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("should reset elapsedAtPause when adding time to active player while paused", () => {
|
|
346
|
+
const state = createState({
|
|
347
|
+
status: "paused",
|
|
348
|
+
activePlayer: "white",
|
|
349
|
+
elapsedAtPause: 2000,
|
|
350
|
+
});
|
|
351
|
+
const result = clockReducer(state, {
|
|
352
|
+
type: "ADD_TIME",
|
|
353
|
+
payload: { player: "white", milliseconds: 5000 },
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
expect(result.times.white).toBe(305000);
|
|
357
|
+
expect(result.elapsedAtPause).toBe(0);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("should not reset elapsedAtPause when adding time to non-active player while paused", () => {
|
|
361
|
+
const state = createState({
|
|
362
|
+
status: "paused",
|
|
363
|
+
activePlayer: "white",
|
|
364
|
+
elapsedAtPause: 2000,
|
|
365
|
+
});
|
|
366
|
+
const result = clockReducer(state, {
|
|
367
|
+
type: "ADD_TIME",
|
|
368
|
+
payload: { player: "black", milliseconds: 5000 },
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
expect(result.times.black).toBe(305000);
|
|
372
|
+
expect(result.elapsedAtPause).toBe(2000);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("should account for added time on resume after pause", () => {
|
|
376
|
+
// Simulate: running -> pause after 2s -> add 5s to active player -> resume
|
|
377
|
+
const startTime = 1000000;
|
|
378
|
+
let state = createState({
|
|
379
|
+
status: "running",
|
|
380
|
+
activePlayer: "white",
|
|
381
|
+
moveStartTime: startTime,
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Pause after 2 seconds
|
|
385
|
+
const pauseTime = startTime + 2000;
|
|
386
|
+
state = clockReducer(state, {
|
|
387
|
+
type: "PAUSE",
|
|
388
|
+
payload: { now: pauseTime },
|
|
389
|
+
});
|
|
390
|
+
expect(state.elapsedAtPause).toBe(2000);
|
|
391
|
+
|
|
392
|
+
// Add 5 seconds while paused
|
|
393
|
+
state = clockReducer(state, {
|
|
394
|
+
type: "ADD_TIME",
|
|
395
|
+
payload: { player: "white", milliseconds: 5000 },
|
|
396
|
+
});
|
|
397
|
+
expect(state.elapsedAtPause).toBe(0);
|
|
398
|
+
|
|
399
|
+
// Resume - moveStartTime should equal now (no stale offset)
|
|
400
|
+
const resumeTime = pauseTime + 1000;
|
|
401
|
+
state = clockReducer(state, {
|
|
402
|
+
type: "RESUME",
|
|
403
|
+
payload: { now: resumeTime },
|
|
404
|
+
});
|
|
405
|
+
expect(state.moveStartTime).toBe(resumeTime);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
describe("SET_TIME", () => {
|
|
410
|
+
it("should set time for specified player", () => {
|
|
411
|
+
const state = createState();
|
|
412
|
+
const result = clockReducer(state, {
|
|
413
|
+
type: "SET_TIME",
|
|
414
|
+
payload: { player: "white", milliseconds: 120000 },
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
expect(result.times.white).toBe(120000);
|
|
418
|
+
expect(result.times.black).toBe(300000);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it("should clamp negative values to zero", () => {
|
|
422
|
+
const state = createState();
|
|
423
|
+
const result = clockReducer(state, {
|
|
424
|
+
type: "SET_TIME",
|
|
425
|
+
payload: { player: "white", milliseconds: -5000 },
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
expect(result.times.white).toBe(0);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("should reset elapsedAtPause when setting time for active player while paused", () => {
|
|
432
|
+
const state = createState({
|
|
433
|
+
status: "paused",
|
|
434
|
+
activePlayer: "white",
|
|
435
|
+
elapsedAtPause: 2000,
|
|
436
|
+
});
|
|
437
|
+
const result = clockReducer(state, {
|
|
438
|
+
type: "SET_TIME",
|
|
439
|
+
payload: { player: "white", milliseconds: 120000 },
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
expect(result.times.white).toBe(120000);
|
|
443
|
+
expect(result.elapsedAtPause).toBe(0);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("should not reset elapsedAtPause when setting time for non-active player while paused", () => {
|
|
447
|
+
const state = createState({
|
|
448
|
+
status: "paused",
|
|
449
|
+
activePlayer: "white",
|
|
450
|
+
elapsedAtPause: 2000,
|
|
451
|
+
});
|
|
452
|
+
const result = clockReducer(state, {
|
|
453
|
+
type: "SET_TIME",
|
|
454
|
+
payload: { player: "black", milliseconds: 120000 },
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
expect(result.times.black).toBe(120000);
|
|
458
|
+
expect(result.elapsedAtPause).toBe(2000);
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
describe("unknown action", () => {
|
|
463
|
+
it("should return state unchanged for unknown action", () => {
|
|
464
|
+
const state = createState();
|
|
465
|
+
// @ts-expect-error - testing unknown action
|
|
466
|
+
const result = clockReducer(state, { type: "UNKNOWN" });
|
|
467
|
+
|
|
468
|
+
expect(result).toBe(state);
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
describe("createInitialClockState", () => {
|
|
474
|
+
const testConfig = parseTimeControlConfig({ time: "5+0" });
|
|
475
|
+
|
|
476
|
+
it("should create initial state with provided values", () => {
|
|
477
|
+
const initialTimes = { white: 300000, black: 300000 };
|
|
478
|
+
const state = createInitialClockState(
|
|
479
|
+
initialTimes,
|
|
480
|
+
"running",
|
|
481
|
+
"white",
|
|
482
|
+
testConfig,
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
expect(state).toEqual({
|
|
486
|
+
times: initialTimes,
|
|
487
|
+
initialTimes,
|
|
488
|
+
status: "running",
|
|
489
|
+
activePlayer: "white",
|
|
490
|
+
timeout: null,
|
|
491
|
+
switchCount: 0,
|
|
492
|
+
periodState: undefined,
|
|
493
|
+
config: testConfig,
|
|
494
|
+
moveStartTime: expect.any(Number),
|
|
495
|
+
elapsedAtPause: 0,
|
|
496
|
+
});
|
|
497
|
+
expect(state.moveStartTime).toBeGreaterThan(0);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it("should handle delayed status", () => {
|
|
501
|
+
const initialTimes = { white: 600000, black: 600000 };
|
|
502
|
+
const state = createInitialClockState(
|
|
503
|
+
initialTimes,
|
|
504
|
+
"delayed",
|
|
505
|
+
null,
|
|
506
|
+
testConfig,
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
expect(state.status).toBe("delayed");
|
|
510
|
+
expect(state.activePlayer).toBeNull();
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it("should include periodState when provided", () => {
|
|
514
|
+
const initialTimes = { white: 300000, black: 300000 };
|
|
515
|
+
const periodState: PeriodState = {
|
|
516
|
+
periodIndex: { white: 0, black: 0 },
|
|
517
|
+
periodMoves: { white: 0, black: 0 },
|
|
518
|
+
periods: [
|
|
519
|
+
{ baseTime: 5_400_000, increment: 30_000, moves: 40 },
|
|
520
|
+
{ baseTime: 1_800_000, increment: 30_000 },
|
|
521
|
+
],
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const state = createInitialClockState(
|
|
525
|
+
initialTimes,
|
|
526
|
+
"running",
|
|
527
|
+
"white",
|
|
528
|
+
testConfig,
|
|
529
|
+
periodState,
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
expect(state.periodState).toEqual(periodState);
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// ============================================================================
|
|
537
|
+
// Multi-Period Time Control Tests
|
|
538
|
+
// ============================================================================
|
|
539
|
+
|
|
540
|
+
describe("clockReducer - multi-period time controls", () => {
|
|
541
|
+
const initialTimes = { white: 300000, black: 300000 };
|
|
542
|
+
const multiPeriodConfig = parseTimeControlConfig({
|
|
543
|
+
time: [
|
|
544
|
+
{ baseTime: 5400, increment: 30, moves: 40 },
|
|
545
|
+
{ baseTime: 1800, increment: 30 },
|
|
546
|
+
],
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
function createState(overrides?: Partial<ClockState>): ClockState {
|
|
550
|
+
const { moveStartTime, elapsedAtPause, ...rest } = overrides ?? {};
|
|
551
|
+
return {
|
|
552
|
+
times: initialTimes,
|
|
553
|
+
initialTimes,
|
|
554
|
+
status: "idle",
|
|
555
|
+
activePlayer: null,
|
|
556
|
+
timeout: null,
|
|
557
|
+
switchCount: 0,
|
|
558
|
+
config: multiPeriodConfig,
|
|
559
|
+
moveStartTime: moveStartTime ?? null,
|
|
560
|
+
elapsedAtPause: elapsedAtPause ?? 0,
|
|
561
|
+
...rest,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function createPeriodState(
|
|
566
|
+
periodIndex: { white: number; black: number },
|
|
567
|
+
periodMoves: { white: number; black: number },
|
|
568
|
+
periods: PeriodState["periods"],
|
|
569
|
+
): PeriodState {
|
|
570
|
+
return {
|
|
571
|
+
periodIndex,
|
|
572
|
+
periodMoves,
|
|
573
|
+
periods,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
describe("period advancement - both players simultaneously", () => {
|
|
578
|
+
it("should advance both players to next period when both complete required moves", () => {
|
|
579
|
+
const periods = [
|
|
580
|
+
{ baseTime: 5_400_000, increment: 30_000, moves: 40 },
|
|
581
|
+
{ baseTime: 1_800_000, increment: 30_000 },
|
|
582
|
+
];
|
|
583
|
+
|
|
584
|
+
const periodState = createPeriodState(
|
|
585
|
+
{ white: 0, black: 0 },
|
|
586
|
+
{ white: 40, black: 40 },
|
|
587
|
+
periods,
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
const state = createState({
|
|
591
|
+
status: "running",
|
|
592
|
+
activePlayer: "white",
|
|
593
|
+
periodState,
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
const result = clockReducer(state, {
|
|
597
|
+
type: "SWITCH",
|
|
598
|
+
payload: { newTimes: { white: 100000, black: 100000 } },
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
// Both players advance to period 1
|
|
602
|
+
expect(result.periodState?.periodIndex).toEqual({ white: 1, black: 1 });
|
|
603
|
+
// Both players have move counters reset
|
|
604
|
+
expect(result.periodState?.periodMoves).toEqual({ white: 0, black: 0 });
|
|
605
|
+
// Both players receive next period's base time (1,800,000ms)
|
|
606
|
+
expect(result.times.white).toBe(100000 + 1_800_000);
|
|
607
|
+
expect(result.times.black).toBe(100000 + 1_800_000);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it("should add correct base time when advancing", () => {
|
|
611
|
+
const periods = [
|
|
612
|
+
{ baseTime: 300_000, increment: 5_000, moves: 5 },
|
|
613
|
+
{ baseTime: 180_000, increment: 3_000 },
|
|
614
|
+
];
|
|
615
|
+
|
|
616
|
+
const periodState = createPeriodState(
|
|
617
|
+
{ white: 0, black: 0 },
|
|
618
|
+
{ white: 5, black: 5 },
|
|
619
|
+
periods,
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
const state = createState({
|
|
623
|
+
status: "running",
|
|
624
|
+
activePlayer: "white",
|
|
625
|
+
periodState,
|
|
626
|
+
times: { white: 150000, black: 150000 },
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
const result = clockReducer(state, {
|
|
630
|
+
type: "SWITCH",
|
|
631
|
+
payload: { newTimes: { white: 150000, black: 150000 } },
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
// Each player gets 180,000ms added
|
|
635
|
+
expect(result.times.white).toBe(150000 + 180_000);
|
|
636
|
+
expect(result.times.black).toBe(150000 + 180_000);
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
describe("period advancement - players at different rates", () => {
|
|
641
|
+
it("should advance only white when white completes required moves but black does not", () => {
|
|
642
|
+
const periods = [
|
|
643
|
+
{ baseTime: 5_400_000, increment: 30_000, moves: 40 },
|
|
644
|
+
{ baseTime: 1_800_000, increment: 30_000 },
|
|
645
|
+
];
|
|
646
|
+
|
|
647
|
+
const periodState = createPeriodState(
|
|
648
|
+
{ white: 0, black: 0 },
|
|
649
|
+
{ white: 40, black: 35 },
|
|
650
|
+
periods,
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
const state = createState({
|
|
654
|
+
status: "running",
|
|
655
|
+
activePlayer: "white",
|
|
656
|
+
periodState,
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
const result = clockReducer(state, {
|
|
660
|
+
type: "SWITCH",
|
|
661
|
+
payload: { newTimes: { white: 100000, black: 100000 } },
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
// White advances, black stays in period 0
|
|
665
|
+
expect(result.periodState?.periodIndex).toEqual({ white: 1, black: 0 });
|
|
666
|
+
// White's move counter resets, black's continues
|
|
667
|
+
expect(result.periodState?.periodMoves).toEqual({ white: 0, black: 35 });
|
|
668
|
+
// Only white receives additional time
|
|
669
|
+
expect(result.times.white).toBe(100000 + 1_800_000);
|
|
670
|
+
expect(result.times.black).toBe(100000);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it("should advance only black when black completes required moves but white does not", () => {
|
|
674
|
+
const periods = [
|
|
675
|
+
{ baseTime: 5_400_000, increment: 30_000, moves: 40 },
|
|
676
|
+
{ baseTime: 1_800_000, increment: 30_000 },
|
|
677
|
+
];
|
|
678
|
+
|
|
679
|
+
const periodState = createPeriodState(
|
|
680
|
+
{ white: 0, black: 0 },
|
|
681
|
+
{ white: 35, black: 40 },
|
|
682
|
+
periods,
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
const state = createState({
|
|
686
|
+
status: "running",
|
|
687
|
+
activePlayer: "black",
|
|
688
|
+
periodState,
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
const result = clockReducer(state, {
|
|
692
|
+
type: "SWITCH",
|
|
693
|
+
payload: { newTimes: { white: 100000, black: 100000 } },
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
expect(result.periodState?.periodIndex).toEqual({ white: 0, black: 1 });
|
|
697
|
+
expect(result.periodState?.periodMoves).toEqual({ white: 35, black: 0 });
|
|
698
|
+
expect(result.times.white).toBe(100000);
|
|
699
|
+
expect(result.times.black).toBe(100000 + 1_800_000);
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it("should handle gradual catch-up - white advances first, black catches up later", () => {
|
|
703
|
+
const periods = [
|
|
704
|
+
{ baseTime: 300_000, increment: 5_000, moves: 3 },
|
|
705
|
+
{ baseTime: 180_000, increment: 3_000 },
|
|
706
|
+
];
|
|
707
|
+
|
|
708
|
+
// Initial: white has 2 moves, black has 2 moves (one away from advancing)
|
|
709
|
+
const periodState = createPeriodState(
|
|
710
|
+
{ white: 0, black: 0 },
|
|
711
|
+
{ white: 2, black: 2 },
|
|
712
|
+
periods,
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
let state = createState({
|
|
716
|
+
status: "running",
|
|
717
|
+
activePlayer: "white",
|
|
718
|
+
periodState,
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
// First switch - white moves (3rd move), advances to period 1
|
|
722
|
+
let result = clockReducer(state, {
|
|
723
|
+
type: "SWITCH",
|
|
724
|
+
payload: { newTimes: { white: 100000, black: 100000 } },
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
expect(result.periodState?.periodIndex).toEqual({ white: 1, black: 0 });
|
|
728
|
+
expect(result.periodState?.periodMoves).toEqual({ white: 0, black: 2 });
|
|
729
|
+
|
|
730
|
+
// Second switch - black moves (3rd move), advances to period 1
|
|
731
|
+
state = { ...result, times: result.times };
|
|
732
|
+
result = clockReducer(state, {
|
|
733
|
+
type: "SWITCH",
|
|
734
|
+
payload: { newTimes: { white: 1900000, black: 100000 } },
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
// Both players now in period 1
|
|
738
|
+
expect(result.periodState?.periodIndex).toEqual({ white: 1, black: 1 });
|
|
739
|
+
expect(result.periodState?.periodMoves).toEqual({ white: 0, black: 0 });
|
|
740
|
+
});
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
describe("final sudden death period", () => {
|
|
744
|
+
it("should not advance from sudden death period", () => {
|
|
745
|
+
const periods = [
|
|
746
|
+
{ baseTime: 5_400_000, increment: 30_000, moves: 40 },
|
|
747
|
+
{ baseTime: 1_800_000, increment: 30_000 },
|
|
748
|
+
];
|
|
749
|
+
|
|
750
|
+
const periodState = createPeriodState(
|
|
751
|
+
{ white: 1, black: 1 },
|
|
752
|
+
{ white: 50, black: 50 },
|
|
753
|
+
periods,
|
|
754
|
+
);
|
|
755
|
+
|
|
756
|
+
const state = createState({
|
|
757
|
+
status: "running",
|
|
758
|
+
activePlayer: "white",
|
|
759
|
+
periodState,
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
const result = clockReducer(state, {
|
|
763
|
+
type: "SWITCH",
|
|
764
|
+
payload: { newTimes: { white: 100000, black: 100000 } },
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
// No advancement from sudden death
|
|
768
|
+
expect(result.periodState?.periodIndex).toEqual({ white: 1, black: 1 });
|
|
769
|
+
// Move counter continues to increment (white was active, so white's count increases)
|
|
770
|
+
expect(result.periodState?.periodMoves).toEqual({ white: 51, black: 50 });
|
|
771
|
+
// No additional time added
|
|
772
|
+
expect(result.times.white).toBe(100000);
|
|
773
|
+
expect(result.times.black).toBe(100000);
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
it("should handle three-period time control with final sudden death", () => {
|
|
777
|
+
const periods = [
|
|
778
|
+
{ baseTime: 5_400_000, increment: 30_000, moves: 40 },
|
|
779
|
+
{ baseTime: 3_600_000, increment: 30_000, moves: 20 },
|
|
780
|
+
{ baseTime: 900_000, increment: 30_000 },
|
|
781
|
+
];
|
|
782
|
+
|
|
783
|
+
const periodState = createPeriodState(
|
|
784
|
+
{ white: 1, black: 2 },
|
|
785
|
+
{ white: 15, black: 25 },
|
|
786
|
+
periods,
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
const state = createState({
|
|
790
|
+
status: "running",
|
|
791
|
+
activePlayer: "white",
|
|
792
|
+
periodState,
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
const result = clockReducer(state, {
|
|
796
|
+
type: "SWITCH",
|
|
797
|
+
payload: { newTimes: { white: 500000, black: 200000 } },
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
// White in period 1, black in period 2 (sudden death)
|
|
801
|
+
expect(result.periodState?.periodIndex).toEqual({ white: 1, black: 2 });
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
describe("edge cases", () => {
|
|
806
|
+
it("should handle exceeding required moves by more than 1", () => {
|
|
807
|
+
const periods = [
|
|
808
|
+
{ baseTime: 300_000, increment: 5_000, moves: 5 },
|
|
809
|
+
{ baseTime: 180_000, increment: 3_000 },
|
|
810
|
+
];
|
|
811
|
+
|
|
812
|
+
const periodState = createPeriodState(
|
|
813
|
+
{ white: 0, black: 0 },
|
|
814
|
+
{ white: 10, black: 3 },
|
|
815
|
+
periods,
|
|
816
|
+
);
|
|
817
|
+
|
|
818
|
+
const state = createState({
|
|
819
|
+
status: "running",
|
|
820
|
+
activePlayer: "white",
|
|
821
|
+
periodState,
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
const result = clockReducer(state, {
|
|
825
|
+
type: "SWITCH",
|
|
826
|
+
payload: { newTimes: { white: 100000, black: 100000 } },
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// White advances despite having way more moves than required
|
|
830
|
+
expect(result.periodState?.periodIndex).toEqual({ white: 1, black: 0 });
|
|
831
|
+
expect(result.periodState?.periodMoves).toEqual({ white: 0, black: 3 });
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
it("should handle zero moves required (treated as sudden death, no advancement)", () => {
|
|
835
|
+
const periods = [
|
|
836
|
+
{ baseTime: 5_400_000, increment: 30_000, moves: 0 },
|
|
837
|
+
{ baseTime: 1_800_000, increment: 30_000 },
|
|
838
|
+
];
|
|
839
|
+
|
|
840
|
+
const periodState = createPeriodState(
|
|
841
|
+
{ white: 0, black: 0 },
|
|
842
|
+
{ white: 0, black: 0 },
|
|
843
|
+
periods,
|
|
844
|
+
);
|
|
845
|
+
|
|
846
|
+
const state = createState({
|
|
847
|
+
status: "running",
|
|
848
|
+
activePlayer: "white",
|
|
849
|
+
periodState,
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
const result = clockReducer(state, {
|
|
853
|
+
type: "SWITCH",
|
|
854
|
+
payload: { newTimes: { white: 100000, black: 100000 } },
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
// Zero moves is treated as falsy (like undefined), so no advancement occurs
|
|
858
|
+
// This is because !0 === true in the check at line 78
|
|
859
|
+
expect(result.periodState?.periodIndex).toEqual({ white: 0, black: 0 });
|
|
860
|
+
// Move count still increments
|
|
861
|
+
expect(result.periodState?.periodMoves).toEqual({ white: 1, black: 0 });
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
it("should advance when moves equals required after switch", () => {
|
|
865
|
+
const periods = [
|
|
866
|
+
{ baseTime: 5_400_000, increment: 30_000, moves: 40 },
|
|
867
|
+
{ baseTime: 1_800_000, increment: 30_000 },
|
|
868
|
+
];
|
|
869
|
+
|
|
870
|
+
// Start with 39 moves each - one away from advancement
|
|
871
|
+
const periodState = createPeriodState(
|
|
872
|
+
{ white: 0, black: 0 },
|
|
873
|
+
{ white: 39, black: 39 },
|
|
874
|
+
periods,
|
|
875
|
+
);
|
|
876
|
+
|
|
877
|
+
const state = createState({
|
|
878
|
+
status: "running",
|
|
879
|
+
activePlayer: "white",
|
|
880
|
+
periodState,
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
const result = clockReducer(state, {
|
|
884
|
+
type: "SWITCH",
|
|
885
|
+
payload: { newTimes: { white: 100000, black: 100000 } },
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
// White moves (39 -> 40), which meets the requirement, so white advances
|
|
889
|
+
expect(result.periodState?.periodIndex).toEqual({ white: 1, black: 0 });
|
|
890
|
+
expect(result.periodState?.periodMoves).toEqual({ white: 0, black: 39 });
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
describe("delayed mode with multi-period", () => {
|
|
895
|
+
it("should track period moves during delayed mode", () => {
|
|
896
|
+
const periods = [
|
|
897
|
+
{ baseTime: 300_000, increment: 5_000, moves: 2 },
|
|
898
|
+
{ baseTime: 180_000, increment: 3_000 },
|
|
899
|
+
];
|
|
900
|
+
|
|
901
|
+
const periodState = createPeriodState(
|
|
902
|
+
{ white: 0, black: 0 },
|
|
903
|
+
{ white: 0, black: 0 },
|
|
904
|
+
periods,
|
|
905
|
+
);
|
|
906
|
+
|
|
907
|
+
const state = createInitialClockState(
|
|
908
|
+
{ white: 300000, black: 300000 },
|
|
909
|
+
"delayed",
|
|
910
|
+
null,
|
|
911
|
+
multiPeriodConfig,
|
|
912
|
+
periodState,
|
|
913
|
+
);
|
|
914
|
+
|
|
915
|
+
// First switch: white moves (delayed mode)
|
|
916
|
+
let result = clockReducer(state, { type: "SWITCH", payload: {} });
|
|
917
|
+
|
|
918
|
+
expect(result.status).toBe("delayed");
|
|
919
|
+
expect(result.activePlayer).toBe("black");
|
|
920
|
+
expect(result.periodState?.periodMoves).toEqual({ white: 1, black: 0 });
|
|
921
|
+
|
|
922
|
+
// Second switch: black moves (transition to running)
|
|
923
|
+
result = clockReducer(result, { type: "SWITCH", payload: {} });
|
|
924
|
+
|
|
925
|
+
expect(result.status).toBe("running");
|
|
926
|
+
expect(result.activePlayer).toBe("white");
|
|
927
|
+
expect(result.periodState?.periodMoves).toEqual({ white: 1, black: 1 });
|
|
928
|
+
|
|
929
|
+
// Third switch: white moves again (completes 2 moves, advances)
|
|
930
|
+
result = clockReducer(result, {
|
|
931
|
+
type: "SWITCH",
|
|
932
|
+
payload: { newTimes: { white: 300000, black: 300000 } },
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
expect(result.periodState?.periodIndex).toEqual({ white: 1, black: 0 });
|
|
936
|
+
expect(result.periodState?.periodMoves).toEqual({ white: 0, black: 1 });
|
|
937
|
+
});
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
describe("RESET with multi-period state", () => {
|
|
941
|
+
it("should reset period state to initial values", () => {
|
|
942
|
+
const periods = [
|
|
943
|
+
{ baseTime: 5_400_000, increment: 30_000, moves: 40 },
|
|
944
|
+
{ baseTime: 1_800_000, increment: 30_000 },
|
|
945
|
+
];
|
|
946
|
+
|
|
947
|
+
const periodState: PeriodState = {
|
|
948
|
+
periodIndex: { white: 1, black: 0 },
|
|
949
|
+
periodMoves: { white: 15, black: 38 },
|
|
950
|
+
periods,
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
const state = createInitialClockState(
|
|
954
|
+
{ white: 300000, black: 300000 },
|
|
955
|
+
"running",
|
|
956
|
+
"white",
|
|
957
|
+
multiPeriodConfig,
|
|
958
|
+
periodState,
|
|
959
|
+
);
|
|
960
|
+
|
|
961
|
+
const modifiedState = {
|
|
962
|
+
...state,
|
|
963
|
+
times: { white: 100000, black: 200000 },
|
|
964
|
+
status: "paused" as ClockStatus,
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
const result = clockReducer(modifiedState, {
|
|
968
|
+
type: "RESET",
|
|
969
|
+
payload: {
|
|
970
|
+
time: [
|
|
971
|
+
{ baseTime: 5400, increment: 30, moves: 40 },
|
|
972
|
+
{ baseTime: 1800, increment: 30 },
|
|
973
|
+
],
|
|
974
|
+
clockStart: "delayed",
|
|
975
|
+
},
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
expect(result.times).toEqual({ white: 5400000, black: 5400000 });
|
|
979
|
+
expect(result.status).toBe("delayed");
|
|
980
|
+
expect(result.activePlayer).toBeNull();
|
|
981
|
+
expect(result.periodState?.periodIndex).toEqual({ white: 0, black: 0 });
|
|
982
|
+
expect(result.periodState?.periodMoves).toEqual({ white: 0, black: 0 });
|
|
983
|
+
});
|
|
984
|
+
});
|
|
985
|
+
});
|