@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
package/src/types.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Time Control Types
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* String notation for time controls
|
|
7
|
+
* Examples: "5+3" (5 minutes, 3 second increment), "10" (10 minutes, no increment)
|
|
8
|
+
*/
|
|
9
|
+
export type TimeControlString = `${number}+${number}` | `${number}`;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Single period time control configuration
|
|
13
|
+
* Used for standalone games or the final period of tournament controls
|
|
14
|
+
*/
|
|
15
|
+
export interface TimeControl {
|
|
16
|
+
/** Base time in seconds */
|
|
17
|
+
baseTime: number;
|
|
18
|
+
/** Increment in seconds (default: 0) */
|
|
19
|
+
increment?: number;
|
|
20
|
+
/** Delay in seconds (for delay timing methods) */
|
|
21
|
+
delay?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A single time control period for tournament play
|
|
26
|
+
*/
|
|
27
|
+
export interface TimeControlPhase extends TimeControl {
|
|
28
|
+
/** Moves required in this period (undefined = sudden death period) */
|
|
29
|
+
moves?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Time control input - accepts string notation, object configuration, or multi-period array
|
|
34
|
+
*/
|
|
35
|
+
export type TimeControlInput =
|
|
36
|
+
| TimeControlString
|
|
37
|
+
| TimeControl
|
|
38
|
+
| TimeControlPhase[];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Clock timing method
|
|
42
|
+
* - "fischer": Standard increment - adds time after each move
|
|
43
|
+
* - "delay": Simple delay - countdown waits before decrementing
|
|
44
|
+
* - "bronstein": Bronstein delay - adds back actual time used up to delay amount
|
|
45
|
+
*/
|
|
46
|
+
export type TimingMethod = "fischer" | "delay" | "bronstein";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Clock start behavior
|
|
50
|
+
* - "delayed": Clock starts after Black's first move (Lichess-style)
|
|
51
|
+
* - "immediate": White's clock starts immediately (Chess.com-style)
|
|
52
|
+
* - "manual": Clock starts only when user explicitly calls start()
|
|
53
|
+
*/
|
|
54
|
+
export type ClockStartMode = "delayed" | "immediate" | "manual";
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Clock status
|
|
58
|
+
*/
|
|
59
|
+
export type ClockStatus =
|
|
60
|
+
| "idle"
|
|
61
|
+
| "delayed"
|
|
62
|
+
| "running"
|
|
63
|
+
| "paused"
|
|
64
|
+
| "finished";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Player color
|
|
68
|
+
*/
|
|
69
|
+
export type ClockColor = "white" | "black";
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Time control configuration
|
|
73
|
+
*/
|
|
74
|
+
export interface TimeControlConfig {
|
|
75
|
+
/** Time specification (required) */
|
|
76
|
+
time: TimeControlInput;
|
|
77
|
+
/** Override starting time for white (seconds) - for time odds */
|
|
78
|
+
whiteTime?: number;
|
|
79
|
+
/** Override starting time for black (seconds) - for time odds */
|
|
80
|
+
blackTime?: number;
|
|
81
|
+
/** Timing method (default: 'fischer') */
|
|
82
|
+
timingMethod?: TimingMethod;
|
|
83
|
+
/** Clock start behavior (default: 'delayed') */
|
|
84
|
+
clockStart?: ClockStartMode;
|
|
85
|
+
|
|
86
|
+
/** Callback when a player's time runs out */
|
|
87
|
+
onTimeout?: (loser: ClockColor) => void;
|
|
88
|
+
/** Callback when active player switches */
|
|
89
|
+
onSwitch?: (activePlayer: ClockColor) => void;
|
|
90
|
+
/** Callback on each time update */
|
|
91
|
+
onTimeUpdate?: (times: { white: number; black: number }) => void;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Period tracking state (only used for multi-period controls)
|
|
96
|
+
*/
|
|
97
|
+
export interface PeriodState {
|
|
98
|
+
/** Current period index for each player (0-based) */
|
|
99
|
+
periodIndex: {
|
|
100
|
+
white: number;
|
|
101
|
+
black: number;
|
|
102
|
+
};
|
|
103
|
+
/** Moves made in current period by each player */
|
|
104
|
+
periodMoves: {
|
|
105
|
+
white: number;
|
|
106
|
+
black: number;
|
|
107
|
+
};
|
|
108
|
+
/** All periods for this time control */
|
|
109
|
+
periods: TimeControlPhase[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Normalized time control with all times converted to milliseconds
|
|
114
|
+
*/
|
|
115
|
+
export interface NormalizedTimeControl {
|
|
116
|
+
baseTime: number; // milliseconds
|
|
117
|
+
increment: number; // milliseconds
|
|
118
|
+
delay: number; // milliseconds
|
|
119
|
+
timingMethod: TimingMethod;
|
|
120
|
+
clockStart: ClockStartMode;
|
|
121
|
+
whiteTimeOverride?: number; // milliseconds
|
|
122
|
+
blackTimeOverride?: number; // milliseconds
|
|
123
|
+
/** Multi-period configuration (present only for multi-period time controls) */
|
|
124
|
+
periods?: TimeControlPhase[];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// Clock State Types
|
|
129
|
+
// ============================================================================
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Clock times state (used for both current and initial times)
|
|
133
|
+
*/
|
|
134
|
+
export interface ClockTimes {
|
|
135
|
+
white: number; // milliseconds
|
|
136
|
+
black: number; // milliseconds
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Computed clock info
|
|
141
|
+
*/
|
|
142
|
+
export interface ClockInfo {
|
|
143
|
+
isRunning: boolean;
|
|
144
|
+
isPaused: boolean;
|
|
145
|
+
isFinished: boolean;
|
|
146
|
+
isWhiteActive: boolean;
|
|
147
|
+
isBlackActive: boolean;
|
|
148
|
+
hasTimeout: boolean;
|
|
149
|
+
hasTimeOdds: boolean;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Clock methods
|
|
154
|
+
*/
|
|
155
|
+
export interface ClockMethods {
|
|
156
|
+
start: () => void;
|
|
157
|
+
pause: () => void;
|
|
158
|
+
resume: () => void;
|
|
159
|
+
switch: () => void;
|
|
160
|
+
reset: (timeControl?: TimeControlInput) => void;
|
|
161
|
+
addTime: (player: ClockColor, milliseconds: number) => void;
|
|
162
|
+
setTime: (player: ClockColor, milliseconds: number) => void;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Complete clock state return type
|
|
167
|
+
*/
|
|
168
|
+
export interface UseChessClockReturn {
|
|
169
|
+
// Time values (milliseconds)
|
|
170
|
+
times: ClockTimes;
|
|
171
|
+
initialTimes: ClockTimes;
|
|
172
|
+
|
|
173
|
+
// Status
|
|
174
|
+
status: ClockStatus;
|
|
175
|
+
activePlayer: ClockColor | null;
|
|
176
|
+
timeout: ClockColor | null;
|
|
177
|
+
|
|
178
|
+
// Configuration
|
|
179
|
+
timingMethod: TimingMethod;
|
|
180
|
+
|
|
181
|
+
// Computed
|
|
182
|
+
info: ClockInfo;
|
|
183
|
+
|
|
184
|
+
currentPeriodIndex: {
|
|
185
|
+
white: number;
|
|
186
|
+
black: number;
|
|
187
|
+
};
|
|
188
|
+
totalPeriods: number;
|
|
189
|
+
currentPeriod: {
|
|
190
|
+
white: TimeControlPhase;
|
|
191
|
+
black: TimeControlPhase;
|
|
192
|
+
};
|
|
193
|
+
periodMoves: {
|
|
194
|
+
white: number;
|
|
195
|
+
black: number;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Methods
|
|
199
|
+
methods: ClockMethods;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ============================================================================
|
|
203
|
+
// Utility Types
|
|
204
|
+
// ============================================================================
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Time format options
|
|
208
|
+
*/
|
|
209
|
+
export type TimeFormat = "auto" | "mm:ss" | "ss.d" | "hh:mm:ss";
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Timing method result
|
|
213
|
+
*/
|
|
214
|
+
export interface TimingMethodResult {
|
|
215
|
+
newTime: number; // milliseconds
|
|
216
|
+
delayRemaining: number; // milliseconds (for delay methods)
|
|
217
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { calculateSwitchTime } from "../calculateSwitchTime";
|
|
2
|
+
|
|
3
|
+
describe("calculateSwitchTime", () => {
|
|
4
|
+
const baseConfig = {
|
|
5
|
+
baseTime: 300_000,
|
|
6
|
+
increment: 3_000,
|
|
7
|
+
delay: 5_000,
|
|
8
|
+
timingMethod: "fischer" as const,
|
|
9
|
+
clockStart: "delayed" as const,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe("Fischer timing method", () => {
|
|
13
|
+
it("should decrement time spent and add increment", () => {
|
|
14
|
+
const result = calculateSwitchTime(
|
|
15
|
+
300_000, // currentTime
|
|
16
|
+
5_000, // timeSpent
|
|
17
|
+
{ ...baseConfig, timingMethod: "fischer", increment: 3_000 },
|
|
18
|
+
);
|
|
19
|
+
expect(result).toBe(298_000); // 300000 - 5000 + 3000
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should handle time spent less than increment", () => {
|
|
23
|
+
const result = calculateSwitchTime(300_000, 2_000, {
|
|
24
|
+
...baseConfig,
|
|
25
|
+
timingMethod: "fischer",
|
|
26
|
+
increment: 5_000,
|
|
27
|
+
});
|
|
28
|
+
expect(result).toBe(303_000); // 300000 - 2000 + 5000
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should handle zero increment", () => {
|
|
32
|
+
const result = calculateSwitchTime(300_000, 5_000, {
|
|
33
|
+
...baseConfig,
|
|
34
|
+
timingMethod: "fischer",
|
|
35
|
+
increment: 0,
|
|
36
|
+
});
|
|
37
|
+
expect(result).toBe(295_000); // 300000 - 5000
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should not go below zero before increment", () => {
|
|
41
|
+
const result = calculateSwitchTime(1_000, 5_000, {
|
|
42
|
+
...baseConfig,
|
|
43
|
+
timingMethod: "fischer",
|
|
44
|
+
increment: 3_000,
|
|
45
|
+
});
|
|
46
|
+
expect(result).toBe(3_000); // max(0, 1000 - 5000) + 3000
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should handle increment on very low time", () => {
|
|
50
|
+
const result = calculateSwitchTime(100, 5_000, {
|
|
51
|
+
...baseConfig,
|
|
52
|
+
timingMethod: "fischer",
|
|
53
|
+
increment: 3_000,
|
|
54
|
+
});
|
|
55
|
+
expect(result).toBe(3_000); // max(0, 100 - 5000) + 3000 = 0 + 3000
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("Delay timing method", () => {
|
|
60
|
+
it("should not decrement when within delay period", () => {
|
|
61
|
+
const result = calculateSwitchTime(
|
|
62
|
+
300_000,
|
|
63
|
+
3_000, // within 5 second delay
|
|
64
|
+
{ ...baseConfig, timingMethod: "delay", delay: 5_000 },
|
|
65
|
+
);
|
|
66
|
+
expect(result).toBe(300_000); // No decrement (3s - 5s delay = 0 effective)
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should decrement only time spent after delay", () => {
|
|
70
|
+
const result = calculateSwitchTime(
|
|
71
|
+
300_000,
|
|
72
|
+
10_000, // 10 seconds spent, 5 second delay
|
|
73
|
+
{ ...baseConfig, timingMethod: "delay", delay: 5_000 },
|
|
74
|
+
);
|
|
75
|
+
expect(result).toBe(295_000); // 300000 - (10000 - 5000) = 295000
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should handle exactly at delay boundary", () => {
|
|
79
|
+
const result = calculateSwitchTime(
|
|
80
|
+
300_000,
|
|
81
|
+
5_000, // exactly delay amount
|
|
82
|
+
{ ...baseConfig, timingMethod: "delay", delay: 5_000 },
|
|
83
|
+
);
|
|
84
|
+
expect(result).toBe(300_000); // max(0, 5000 - 5000) = 0 effective
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should handle zero delay", () => {
|
|
88
|
+
const result = calculateSwitchTime(300_000, 5_000, {
|
|
89
|
+
...baseConfig,
|
|
90
|
+
timingMethod: "delay",
|
|
91
|
+
delay: 0,
|
|
92
|
+
});
|
|
93
|
+
expect(result).toBe(295_000); // 300000 - 5000
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("Bronstein timing method", () => {
|
|
98
|
+
it("should decrement and add back time spent within delay", () => {
|
|
99
|
+
const result = calculateSwitchTime(
|
|
100
|
+
300_000,
|
|
101
|
+
3_000, // 3 seconds spent, 5 second delay
|
|
102
|
+
{ ...baseConfig, timingMethod: "bronstein", delay: 5_000 },
|
|
103
|
+
);
|
|
104
|
+
expect(result).toBe(300_000); // 300000 - 3000 + 3000 = 300000
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should add back only delay amount when over delay", () => {
|
|
108
|
+
const result = calculateSwitchTime(
|
|
109
|
+
300_000,
|
|
110
|
+
10_000, // 10 seconds spent, 5 second delay
|
|
111
|
+
{ ...baseConfig, timingMethod: "bronstein", delay: 5_000 },
|
|
112
|
+
);
|
|
113
|
+
expect(result).toBe(295_000); // 300000 - 10000 + 5000 = 295000
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should handle zero delay", () => {
|
|
117
|
+
const result = calculateSwitchTime(300_000, 5_000, {
|
|
118
|
+
...baseConfig,
|
|
119
|
+
timingMethod: "bronstein",
|
|
120
|
+
delay: 0,
|
|
121
|
+
});
|
|
122
|
+
expect(result).toBe(295_000); // 300000 - 5000 + 0 = 295000
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("Edge cases", () => {
|
|
127
|
+
it("should handle zero time spent", () => {
|
|
128
|
+
const result = calculateSwitchTime(300_000, 0, baseConfig);
|
|
129
|
+
expect(result).toBe(303_000); // 300000 - 0 + 3000 (increment)
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should handle very low current time", () => {
|
|
133
|
+
const result = calculateSwitchTime(500, 3_000, {
|
|
134
|
+
...baseConfig,
|
|
135
|
+
timingMethod: "fischer",
|
|
136
|
+
increment: 2_000,
|
|
137
|
+
});
|
|
138
|
+
expect(result).toBe(2_000); // max(0, 500 - 3000) + 2000 = 0 + 2000
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should handle zero current time", () => {
|
|
142
|
+
const result = calculateSwitchTime(0, 3_000, {
|
|
143
|
+
...baseConfig,
|
|
144
|
+
timingMethod: "fischer",
|
|
145
|
+
increment: 2_000,
|
|
146
|
+
});
|
|
147
|
+
expect(result).toBe(2_000); // max(0, 0 - 3000) + 2000 = 0 + 2000
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { formatClockTime } from "../formatTime";
|
|
2
|
+
|
|
3
|
+
describe("formatClockTime", () => {
|
|
4
|
+
describe("auto format", () => {
|
|
5
|
+
it("should show mm:ss for times >= 20 seconds", () => {
|
|
6
|
+
expect(formatClockTime(20_000, "auto")).toBe("0:20");
|
|
7
|
+
expect(formatClockTime(60_000, "auto")).toBe("1:00");
|
|
8
|
+
expect(formatClockTime(300_000, "auto")).toBe("5:00");
|
|
9
|
+
expect(formatClockTime(600_000, "auto")).toBe("10:00");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("should show ss.d (seconds with decimal) for times < 20 seconds", () => {
|
|
13
|
+
expect(formatClockTime(19_999, "auto")).toBe("20.0");
|
|
14
|
+
expect(formatClockTime(10_500, "auto")).toBe("10.5");
|
|
15
|
+
expect(formatClockTime(5_000, "auto")).toBe("5.0");
|
|
16
|
+
expect(formatClockTime(500, "auto")).toBe("0.5");
|
|
17
|
+
expect(formatClockTime(100, "auto")).toBe("0.1");
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("mm:ss format", () => {
|
|
22
|
+
it("should format time as minutes:seconds", () => {
|
|
23
|
+
expect(formatClockTime(0, "mm:ss")).toBe("0:00");
|
|
24
|
+
expect(formatClockTime(5_000, "mm:ss")).toBe("0:05");
|
|
25
|
+
expect(formatClockTime(60_000, "mm:ss")).toBe("1:00");
|
|
26
|
+
expect(formatClockTime(65_000, "mm:ss")).toBe("1:05");
|
|
27
|
+
expect(formatClockTime(300_000, "mm:ss")).toBe("5:00");
|
|
28
|
+
expect(formatClockTime(365_000, "mm:ss")).toBe("6:05");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should show hours when time exceeds 60 minutes", () => {
|
|
32
|
+
expect(formatClockTime(3_600_000, "mm:ss")).toBe("1:00:00");
|
|
33
|
+
expect(formatClockTime(3_665_000, "mm:ss")).toBe("1:01:05");
|
|
34
|
+
expect(formatClockTime(5_400_000, "mm:ss")).toBe("1:30:00");
|
|
35
|
+
expect(formatClockTime(7_200_000, "mm:ss")).toBe("2:00:00");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("ss.d format", () => {
|
|
40
|
+
it("should format time as seconds with decimal", () => {
|
|
41
|
+
expect(formatClockTime(0, "ss.d")).toBe("0.0");
|
|
42
|
+
expect(formatClockTime(500, "ss.d")).toBe("0.5");
|
|
43
|
+
expect(formatClockTime(5_000, "ss.d")).toBe("5.0");
|
|
44
|
+
expect(formatClockTime(10_500, "ss.d")).toBe("10.5");
|
|
45
|
+
expect(formatClockTime(60_000, "ss.d")).toBe("60.0");
|
|
46
|
+
expect(formatClockTime(125_500, "ss.d")).toBe("125.5");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("hh:mm:ss format", () => {
|
|
51
|
+
it("should format time as hours:minutes:seconds", () => {
|
|
52
|
+
expect(formatClockTime(0, "hh:mm:ss")).toBe("0:00:00");
|
|
53
|
+
expect(formatClockTime(5_000, "hh:mm:ss")).toBe("0:00:05");
|
|
54
|
+
expect(formatClockTime(65_000, "hh:mm:ss")).toBe("0:01:05");
|
|
55
|
+
expect(formatClockTime(3_600_000, "hh:mm:ss")).toBe("1:00:00");
|
|
56
|
+
expect(formatClockTime(3_665_000, "hh:mm:ss")).toBe("1:01:05");
|
|
57
|
+
expect(formatClockTime(5_400_000, "hh:mm:ss")).toBe("1:30:00");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("edge cases", () => {
|
|
62
|
+
it("should clamp negative times to zero", () => {
|
|
63
|
+
expect(formatClockTime(-1000, "auto")).toBe("0.0");
|
|
64
|
+
expect(formatClockTime(-1000, "mm:ss")).toBe("0:00");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should handle zero time", () => {
|
|
68
|
+
expect(formatClockTime(0, "auto")).toBe("0.0");
|
|
69
|
+
expect(formatClockTime(0, "mm:ss")).toBe("0:00");
|
|
70
|
+
expect(formatClockTime(0, "ss.d")).toBe("0.0");
|
|
71
|
+
expect(formatClockTime(0, "hh:mm:ss")).toBe("0:00:00");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should ceil to nearest second for mm:ss and hh:mm:ss", () => {
|
|
75
|
+
// 5999ms should round up to 6 seconds
|
|
76
|
+
expect(formatClockTime(5_999, "mm:ss")).toBe("0:06");
|
|
77
|
+
expect(formatClockTime(5_999, "hh:mm:ss")).toBe("0:00:06");
|
|
78
|
+
|
|
79
|
+
// 5001ms should round up
|
|
80
|
+
expect(formatClockTime(5_001, "mm:ss")).toBe("0:06");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|