@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +697 -0
  3. package/dist/index.cjs +1014 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +528 -0
  6. package/dist/index.d.ts +528 -0
  7. package/dist/index.js +969 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +63 -0
  10. package/src/components/ChessClock/ChessClock.stories.tsx +782 -0
  11. package/src/components/ChessClock/index.ts +44 -0
  12. package/src/components/ChessClock/parts/Display.tsx +69 -0
  13. package/src/components/ChessClock/parts/PlayPause.tsx +190 -0
  14. package/src/components/ChessClock/parts/Reset.tsx +90 -0
  15. package/src/components/ChessClock/parts/Root.tsx +37 -0
  16. package/src/components/ChessClock/parts/Switch.tsx +84 -0
  17. package/src/components/ChessClock/parts/__tests__/Display.test.tsx +149 -0
  18. package/src/components/ChessClock/parts/__tests__/PlayPause.test.tsx +411 -0
  19. package/src/components/ChessClock/parts/__tests__/Reset.test.tsx +160 -0
  20. package/src/components/ChessClock/parts/__tests__/Root.test.tsx +49 -0
  21. package/src/components/ChessClock/parts/__tests__/Switch.test.tsx +204 -0
  22. package/src/hooks/__tests__/clockReducer.test.ts +985 -0
  23. package/src/hooks/__tests__/useChessClock.test.tsx +1080 -0
  24. package/src/hooks/clockReducer.ts +379 -0
  25. package/src/hooks/useChessClock.ts +406 -0
  26. package/src/hooks/useChessClockContext.ts +35 -0
  27. package/src/index.ts +65 -0
  28. package/src/types.ts +217 -0
  29. package/src/utils/__tests__/calculateSwitchTime.test.ts +150 -0
  30. package/src/utils/__tests__/formatTime.test.ts +83 -0
  31. package/src/utils/__tests__/timeControl.test.ts +414 -0
  32. package/src/utils/__tests__/timingMethods.test.ts +170 -0
  33. package/src/utils/calculateSwitchTime.ts +37 -0
  34. package/src/utils/formatTime.ts +59 -0
  35. package/src/utils/presets.ts +47 -0
  36. package/src/utils/timeControl.ts +205 -0
  37. package/src/utils/timingMethods.ts +103 -0
@@ -0,0 +1,44 @@
1
+ import { Root } from "./parts/Root";
2
+ import { Display } from "./parts/Display";
3
+ import { Switch } from "./parts/Switch";
4
+ import { PlayPause } from "./parts/PlayPause";
5
+ import { Reset } from "./parts/Reset";
6
+
7
+ /**
8
+ * ChessClock compound components
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * import { ChessClock } from "@react-chess-tools/react-chess-clock";
13
+ *
14
+ * function ClockApp() {
15
+ * return (
16
+ * <ChessClock.Root timeControl={{ time: "5+3" }}>
17
+ * <ChessClock.Display color="black" />
18
+ * <ChessClock.Display color="white" />
19
+ * <ChessClock.Switch>Switch</ChessClock.Switch>
20
+ * <ChessClock.PlayPause
21
+ * startContent="Start"
22
+ * pauseContent="Pause"
23
+ * resumeContent="Resume"
24
+ * />
25
+ * <ChessClock.Reset>Reset</ChessClock.Reset>
26
+ * </ChessClock.Root>
27
+ * );
28
+ * }
29
+ * ```
30
+ */
31
+ export const ChessClock = {
32
+ Root,
33
+ Display,
34
+ Switch,
35
+ PlayPause,
36
+ Reset,
37
+ };
38
+
39
+ // Re-export types for convenience
40
+ export type { ChessClockRootProps } from "./parts/Root";
41
+ export type { ChessClockDisplayProps } from "./parts/Display";
42
+ export type { ChessClockControlProps } from "./parts/Switch";
43
+ export type { ChessClockPlayPauseProps } from "./parts/PlayPause";
44
+ export type { ChessClockResetProps } from "./parts/Reset";
@@ -0,0 +1,69 @@
1
+ import React from "react";
2
+ import type { ClockColor } from "../../../types";
3
+ import { useChessClockContext } from "../../../hooks/useChessClockContext";
4
+ import { formatClockTime } from "../../../utils/formatTime";
5
+
6
+ export interface ChessClockDisplayProps extends React.HTMLAttributes<HTMLDivElement> {
7
+ color: ClockColor;
8
+ format?: "auto" | "mm:ss" | "ss.d" | "hh:mm:ss";
9
+ formatTime?: (milliseconds: number) => string;
10
+ }
11
+
12
+ /**
13
+ * ChessClock.Display - Displays the current time for a player
14
+ *
15
+ * Renders an unstyled div with data attributes for custom styling.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * <ChessClock.Display color="white" format="auto" />
20
+ * <ChessClock.Display color="black" format="ss.d" />
21
+ * <ChessClock.Display
22
+ * color="white"
23
+ * formatTime={(ms) => `${Math.ceil(ms / 1000)}s`}
24
+ * />
25
+ * ```
26
+ */
27
+ export const Display = React.forwardRef<HTMLDivElement, ChessClockDisplayProps>(
28
+ (
29
+ {
30
+ color,
31
+ format = "auto",
32
+ formatTime: customFormatTime,
33
+ className,
34
+ style,
35
+ ...rest
36
+ },
37
+ ref,
38
+ ) => {
39
+ const { times, activePlayer, status, timeout } = useChessClockContext();
40
+
41
+ const time = times[color];
42
+ const isActive = activePlayer === color;
43
+ const hasTimeout = timeout === color;
44
+ const isPaused = status === "paused";
45
+
46
+ // Format the time for display
47
+ const formattedTime = customFormatTime
48
+ ? customFormatTime(time)
49
+ : formatClockTime(time, format);
50
+
51
+ return (
52
+ <div
53
+ ref={ref}
54
+ className={className}
55
+ style={style}
56
+ data-clock-color={color}
57
+ data-clock-active={isActive ? "true" : "false"}
58
+ data-clock-paused={isPaused ? "true" : "false"}
59
+ data-clock-timeout={hasTimeout ? "true" : "false"}
60
+ data-clock-status={status}
61
+ {...rest}
62
+ >
63
+ {formattedTime}
64
+ </div>
65
+ );
66
+ },
67
+ );
68
+
69
+ Display.displayName = "ChessClock.Display";
@@ -0,0 +1,190 @@
1
+ import React from "react";
2
+ import type { ReactNode } from "react";
3
+ import { Slot } from "@radix-ui/react-slot";
4
+ import { useChessClockContext } from "../../../hooks/useChessClockContext";
5
+
6
+ // Default content for each clock state
7
+ const DEFAULT_CONTENT = {
8
+ start: "Start",
9
+ pause: "Pause",
10
+ resume: "Resume",
11
+ delayed: "Start",
12
+ finished: "Game Over",
13
+ } as const;
14
+
15
+ /**
16
+ * Resolves the appropriate content to display based on clock state
17
+ * and custom content props. Custom content takes precedence over defaults.
18
+ */
19
+ const resolveContent = (
20
+ isFinished: boolean,
21
+ isDelayed: boolean,
22
+ shouldShowStart: boolean,
23
+ isPaused: boolean,
24
+ isRunning: boolean,
25
+ customContent: {
26
+ startContent?: ReactNode;
27
+ pauseContent?: ReactNode;
28
+ resumeContent?: ReactNode;
29
+ delayedContent?: ReactNode;
30
+ finishedContent?: ReactNode;
31
+ },
32
+ ): ReactNode => {
33
+ if (isFinished) {
34
+ return customContent.finishedContent ?? DEFAULT_CONTENT.finished;
35
+ }
36
+ if (isDelayed) {
37
+ return customContent.delayedContent ?? DEFAULT_CONTENT.delayed;
38
+ }
39
+ if (shouldShowStart) {
40
+ return customContent.startContent ?? DEFAULT_CONTENT.start;
41
+ }
42
+ if (isPaused) {
43
+ return customContent.resumeContent ?? DEFAULT_CONTENT.resume;
44
+ }
45
+ if (isRunning) {
46
+ return customContent.pauseContent ?? DEFAULT_CONTENT.pause;
47
+ }
48
+ return DEFAULT_CONTENT.start;
49
+ };
50
+
51
+ export interface ChessClockPlayPauseProps extends Omit<
52
+ React.ButtonHTMLAttributes<HTMLButtonElement>,
53
+ "onClick"
54
+ > {
55
+ asChild?: boolean;
56
+ children?: ReactNode;
57
+ onClick?: React.MouseEventHandler<HTMLElement>;
58
+ /** Content shown when clock is idle (not yet started) - clicking will start the clock */
59
+ startContent?: ReactNode;
60
+ /** Content shown when clock is running - clicking will pause */
61
+ pauseContent?: ReactNode;
62
+ /** Content shown when clock is paused - clicking will resume */
63
+ resumeContent?: ReactNode;
64
+ /** Content shown when clock is in delayed mode - clicking will start the clock immediately */
65
+ delayedContent?: ReactNode;
66
+ /** Content shown when clock is finished - button is disabled but shows this content */
67
+ finishedContent?: ReactNode;
68
+ }
69
+
70
+ /**
71
+ * ChessClock.PlayPause - Button to start, pause, and resume the clock
72
+ *
73
+ * Supports the asChild pattern and conditional content based on clock state.
74
+ * When no children or custom content is provided, sensible defaults are used for each state.
75
+ *
76
+ * @example
77
+ * ```tsx
78
+ * // With children (backward compatible)
79
+ * <ChessClock.PlayPause>
80
+ * <span>Toggle</span>
81
+ * </ChessClock.PlayPause>
82
+ *
83
+ * // No props - uses defaults for all states
84
+ * <ChessClock.PlayPause />
85
+ * // Shows: "Start" → "Pause" → "Resume" → "Game Over"
86
+ *
87
+ * // Override just one state, others use defaults
88
+ * <ChessClock.PlayPause pauseContent="⏸️ Stop" />
89
+ * // Shows: "Start" → "⏸️ Stop" → "Resume" → "Game Over"
90
+ *
91
+ * // As child
92
+ * <ChessClock.PlayPause asChild>
93
+ * <div className="custom-button">Toggle</div>
94
+ * </ChessClock.PlayPause>
95
+ * ```
96
+ */
97
+ export const PlayPause = React.forwardRef<
98
+ HTMLElement,
99
+ React.PropsWithChildren<ChessClockPlayPauseProps>
100
+ >(
101
+ (
102
+ {
103
+ asChild = false,
104
+ startContent,
105
+ pauseContent,
106
+ resumeContent,
107
+ delayedContent,
108
+ finishedContent,
109
+ children,
110
+ onClick,
111
+ disabled,
112
+ className,
113
+ style,
114
+ type,
115
+ ...rest
116
+ },
117
+ ref,
118
+ ) => {
119
+ const { status, methods } = useChessClockContext();
120
+ const isIdle = status === "idle";
121
+ const isDelayed = status === "delayed";
122
+ const isPaused = status === "paused";
123
+ const isRunning = status === "running";
124
+ const isFinished = status === "finished";
125
+
126
+ // Treat "delayed" like "idle" - clock hasn't started yet
127
+ const shouldShowStart = isIdle || isDelayed;
128
+
129
+ const isDisabled = disabled || isFinished || isDelayed;
130
+
131
+ const handleClick = React.useCallback(
132
+ (e: React.MouseEvent<HTMLElement>) => {
133
+ if (shouldShowStart) {
134
+ methods.start();
135
+ } else if (isPaused) {
136
+ methods.resume();
137
+ } else if (isRunning) {
138
+ methods.pause();
139
+ }
140
+ onClick?.(e);
141
+ },
142
+ [shouldShowStart, isPaused, isRunning, methods, onClick],
143
+ );
144
+
145
+ // Determine content to render
146
+ // Priority: children > custom+defaults > all defaults
147
+ const content =
148
+ children ??
149
+ resolveContent(
150
+ isFinished,
151
+ isDelayed,
152
+ shouldShowStart,
153
+ isPaused,
154
+ isRunning,
155
+ {
156
+ startContent,
157
+ pauseContent,
158
+ resumeContent,
159
+ delayedContent,
160
+ finishedContent,
161
+ },
162
+ );
163
+
164
+ return asChild ? (
165
+ <Slot
166
+ ref={ref}
167
+ onClick={handleClick}
168
+ className={className}
169
+ style={style}
170
+ {...{ ...rest, disabled: isDisabled }}
171
+ >
172
+ {content}
173
+ </Slot>
174
+ ) : (
175
+ <button
176
+ ref={ref as React.RefObject<HTMLButtonElement>}
177
+ type={type || "button"}
178
+ className={className}
179
+ style={style}
180
+ onClick={handleClick}
181
+ disabled={isDisabled}
182
+ {...rest}
183
+ >
184
+ {content}
185
+ </button>
186
+ );
187
+ },
188
+ );
189
+
190
+ PlayPause.displayName = "ChessClock.PlayPause";
@@ -0,0 +1,90 @@
1
+ import React from "react";
2
+ import type { ReactNode } from "react";
3
+ import { Slot } from "@radix-ui/react-slot";
4
+ import { useChessClockContext } from "../../../hooks/useChessClockContext";
5
+ import type { TimeControlInput } from "../../../types";
6
+
7
+ export interface ChessClockResetProps extends Omit<
8
+ React.ButtonHTMLAttributes<HTMLButtonElement>,
9
+ "onClick"
10
+ > {
11
+ asChild?: boolean;
12
+ children?: ReactNode;
13
+ onClick?: React.MouseEventHandler<HTMLElement>;
14
+ timeControl?: TimeControlInput;
15
+ }
16
+
17
+ /**
18
+ * ChessClock.Reset - Button to reset the clock
19
+ *
20
+ * Supports the asChild pattern and optional new time control on reset.
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * <ChessClock.Reset>Reset</ChessClock.Reset>
25
+ *
26
+ * // Reset with new time control
27
+ * <ChessClock.Reset timeControl="10+5">Change to 10+5</ChessClock.Reset>
28
+ *
29
+ * // As child
30
+ * <ChessClock.Reset asChild>
31
+ * <div className="custom-reset">Reset</div>
32
+ * </ChessClock.Reset>
33
+ * ```
34
+ */
35
+ export const Reset = React.forwardRef<
36
+ HTMLElement,
37
+ React.PropsWithChildren<ChessClockResetProps>
38
+ >(
39
+ (
40
+ {
41
+ asChild = false,
42
+ timeControl,
43
+ children,
44
+ onClick,
45
+ disabled,
46
+ className,
47
+ style,
48
+ type,
49
+ ...rest
50
+ },
51
+ ref,
52
+ ) => {
53
+ const { methods, status } = useChessClockContext();
54
+ const isDisabled = disabled || status === "idle";
55
+
56
+ const handleClick = React.useCallback(
57
+ (e: React.MouseEvent<HTMLElement>) => {
58
+ methods.reset(timeControl);
59
+ onClick?.(e);
60
+ },
61
+ [methods, timeControl, onClick],
62
+ );
63
+
64
+ return asChild ? (
65
+ <Slot
66
+ ref={ref}
67
+ onClick={handleClick}
68
+ className={className}
69
+ style={style}
70
+ {...{ ...rest, disabled: isDisabled }}
71
+ >
72
+ {children}
73
+ </Slot>
74
+ ) : (
75
+ <button
76
+ ref={ref as React.RefObject<HTMLButtonElement>}
77
+ type={type || "button"}
78
+ className={className}
79
+ style={style}
80
+ onClick={handleClick}
81
+ disabled={isDisabled}
82
+ {...rest}
83
+ >
84
+ {children}
85
+ </button>
86
+ );
87
+ },
88
+ );
89
+
90
+ Reset.displayName = "ChessClock.Reset";
@@ -0,0 +1,37 @@
1
+ import React from "react";
2
+ import type { ReactNode } from "react";
3
+ import { ChessClockContext } from "../../../hooks/useChessClockContext";
4
+ import { useChessClock } from "../../../hooks/useChessClock";
5
+ import type { TimeControlConfig } from "../../../types";
6
+
7
+ export interface ChessClockRootProps {
8
+ timeControl: TimeControlConfig;
9
+ children: ReactNode;
10
+ }
11
+
12
+ /**
13
+ * ChessClock.Root - Context provider for chess clock components
14
+ * Manages clock state and provides it to child components
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * <ChessClock.Root timeControl={{ time: "5+3" }}>
19
+ * <ChessClock.Display color="white" />
20
+ * <ChessClock.Display color="black" />
21
+ * </ChessClock.Root>
22
+ * ```
23
+ */
24
+ export const Root: React.FC<React.PropsWithChildren<ChessClockRootProps>> = ({
25
+ timeControl,
26
+ children,
27
+ }) => {
28
+ const clockState = useChessClock(timeControl);
29
+
30
+ return (
31
+ <ChessClockContext.Provider value={clockState}>
32
+ {children}
33
+ </ChessClockContext.Provider>
34
+ );
35
+ };
36
+
37
+ Root.displayName = "ChessClock.Root";
@@ -0,0 +1,84 @@
1
+ import React from "react";
2
+ import type { ReactNode } from "react";
3
+ import { Slot } from "@radix-ui/react-slot";
4
+ import { useChessClockContext } from "../../../hooks/useChessClockContext";
5
+
6
+ export interface ChessClockControlProps extends Omit<
7
+ React.ButtonHTMLAttributes<HTMLButtonElement>,
8
+ "onClick"
9
+ > {
10
+ asChild?: boolean;
11
+ children?: ReactNode;
12
+ onClick?: React.MouseEventHandler<HTMLElement>;
13
+ }
14
+
15
+ /**
16
+ * ChessClock.Switch - Button to manually switch the active clock
17
+ *
18
+ * Supports the asChild pattern for custom rendering.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * <ChessClock.Switch>Switch Clock</ChessClock.Switch>
23
+ *
24
+ * // As child
25
+ * <ChessClock.Switch asChild>
26
+ * <div className="custom-switch">Switch</div>
27
+ * </ChessClock.Switch>
28
+ * ```
29
+ */
30
+ export const Switch = React.forwardRef<
31
+ HTMLElement,
32
+ React.PropsWithChildren<ChessClockControlProps>
33
+ >(
34
+ (
35
+ {
36
+ asChild = false,
37
+ children,
38
+ onClick,
39
+ disabled,
40
+ className,
41
+ style,
42
+ type,
43
+ ...rest
44
+ },
45
+ ref,
46
+ ) => {
47
+ const { methods, status } = useChessClockContext();
48
+ const isDisabled = disabled || status === "finished" || status === "idle";
49
+
50
+ const handleClick = React.useCallback(
51
+ (e: React.MouseEvent<HTMLElement>) => {
52
+ methods.switch();
53
+ onClick?.(e);
54
+ },
55
+ [methods, onClick],
56
+ );
57
+
58
+ return asChild ? (
59
+ <Slot
60
+ ref={ref}
61
+ onClick={handleClick}
62
+ className={className}
63
+ style={style}
64
+ {...{ ...rest, disabled: isDisabled }}
65
+ >
66
+ {children}
67
+ </Slot>
68
+ ) : (
69
+ <button
70
+ ref={ref as React.RefObject<HTMLButtonElement>}
71
+ type={type || "button"}
72
+ className={className}
73
+ style={style}
74
+ onClick={handleClick}
75
+ disabled={isDisabled}
76
+ {...rest}
77
+ >
78
+ {children}
79
+ </button>
80
+ );
81
+ },
82
+ );
83
+
84
+ Switch.displayName = "ChessClock.Switch";
@@ -0,0 +1,149 @@
1
+ import React from "react";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { ChessClock } from "../../index";
4
+
5
+ describe("ChessClock.Display", () => {
6
+ it("should render white player time", () => {
7
+ render(
8
+ <ChessClock.Root timeControl={{ time: "5+0" }}>
9
+ <ChessClock.Display color="white" data-testid="white-clock" />
10
+ </ChessClock.Root>,
11
+ );
12
+
13
+ const display = screen.getByTestId("white-clock");
14
+ expect(display).toBeInTheDocument();
15
+ expect(display).toHaveTextContent("5:00");
16
+ });
17
+
18
+ it("should render black player time", () => {
19
+ render(
20
+ <ChessClock.Root timeControl={{ time: "10+5" }}>
21
+ <ChessClock.Display color="black" data-testid="black-clock" />
22
+ </ChessClock.Root>,
23
+ );
24
+
25
+ const display = screen.getByTestId("black-clock");
26
+ expect(display).toHaveTextContent("10:00");
27
+ });
28
+
29
+ it("should include data attributes", () => {
30
+ render(
31
+ <ChessClock.Root timeControl={{ time: "5+0" }}>
32
+ <ChessClock.Display color="white" data-testid="clock" />
33
+ </ChessClock.Root>,
34
+ );
35
+
36
+ const display = screen.getByTestId("clock");
37
+ expect(display).toHaveAttribute("data-clock-color", "white");
38
+ expect(display).toHaveAttribute("data-clock-active", "false");
39
+ expect(display).toHaveAttribute("data-clock-paused", "false");
40
+ expect(display).toHaveAttribute("data-clock-timeout", "false");
41
+ expect(display).toHaveAttribute("data-clock-status", "delayed"); // default clockStart is "delayed"
42
+ });
43
+
44
+ it("should show active clock when clock is running", () => {
45
+ render(
46
+ <ChessClock.Root timeControl={{ time: "5+0", clockStart: "immediate" }}>
47
+ <ChessClock.Display color="white" data-testid="white-clock" />
48
+ <ChessClock.Display color="black" data-testid="black-clock" />
49
+ </ChessClock.Root>,
50
+ );
51
+
52
+ const whiteClock = screen.getByTestId("white-clock");
53
+ const blackClock = screen.getByTestId("black-clock");
54
+
55
+ expect(whiteClock).toHaveAttribute("data-clock-active", "true");
56
+ expect(blackClock).toHaveAttribute("data-clock-active", "false");
57
+ });
58
+
59
+ it("should support custom className", () => {
60
+ render(
61
+ <ChessClock.Root timeControl={{ time: "5+0" }}>
62
+ <ChessClock.Display
63
+ color="white"
64
+ className="custom-class"
65
+ data-testid="clock"
66
+ />
67
+ </ChessClock.Root>,
68
+ );
69
+
70
+ expect(screen.getByTestId("clock")).toHaveClass("custom-class");
71
+ });
72
+
73
+ it("should support custom style", () => {
74
+ render(
75
+ <ChessClock.Root timeControl={{ time: "5+0" }}>
76
+ <ChessClock.Display
77
+ color="white"
78
+ style={{ fontSize: "24px" }}
79
+ data-testid="clock"
80
+ />
81
+ </ChessClock.Root>,
82
+ );
83
+
84
+ expect(screen.getByTestId("clock")).toHaveStyle({ fontSize: "24px" });
85
+ });
86
+
87
+ describe("format prop", () => {
88
+ it("should format as mm:ss", () => {
89
+ render(
90
+ <ChessClock.Root timeControl={{ time: "5+0" }}>
91
+ <ChessClock.Display
92
+ color="white"
93
+ format="mm:ss"
94
+ data-testid="clock"
95
+ />
96
+ </ChessClock.Root>,
97
+ );
98
+
99
+ expect(screen.getByTestId("clock")).toHaveTextContent("5:00");
100
+ });
101
+
102
+ it("should format as ss.d", () => {
103
+ render(
104
+ <ChessClock.Root timeControl={{ time: "5+0" }}>
105
+ <ChessClock.Display color="white" format="ss.d" data-testid="clock" />
106
+ </ChessClock.Root>,
107
+ );
108
+
109
+ expect(screen.getByTestId("clock")).toHaveTextContent("300.0");
110
+ });
111
+
112
+ it("should format as hh:mm:ss for long times", () => {
113
+ render(
114
+ <ChessClock.Root timeControl={{ time: "90+30" }}>
115
+ <ChessClock.Display
116
+ color="white"
117
+ format="hh:mm:ss"
118
+ data-testid="clock"
119
+ />
120
+ </ChessClock.Root>,
121
+ );
122
+
123
+ expect(screen.getByTestId("clock")).toHaveTextContent("1:30:00");
124
+ });
125
+ });
126
+
127
+ describe("formatTime prop", () => {
128
+ it("should use custom formatTime function", () => {
129
+ const customFormat = jest.fn((ms) => `${Math.ceil(ms / 1000)}s`);
130
+
131
+ render(
132
+ <ChessClock.Root timeControl={{ time: "5+0" }}>
133
+ <ChessClock.Display
134
+ color="white"
135
+ formatTime={customFormat}
136
+ data-testid="clock"
137
+ />
138
+ </ChessClock.Root>,
139
+ );
140
+
141
+ expect(screen.getByTestId("clock")).toHaveTextContent("300s");
142
+ expect(customFormat).toHaveBeenCalledWith(300_000);
143
+ });
144
+ });
145
+
146
+ it("should have displayName", () => {
147
+ expect(ChessClock.Display.displayName).toBe("ChessClock.Display");
148
+ });
149
+ });