@react-chess-tools/react-chess-puzzle 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +570 -0
- package/dist/index.cjs +132 -107
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -5
- package/dist/index.d.ts +19 -5
- package/dist/index.js +118 -93
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/components/ChessPuzzle/ChessPuzzle.stories.tsx +46 -0
- package/src/components/ChessPuzzle/parts/Hint.tsx +32 -23
- package/src/components/ChessPuzzle/parts/PuzzleBoard.tsx +28 -27
- package/src/components/ChessPuzzle/parts/Reset.tsx +56 -36
- package/src/components/ChessPuzzle/parts/Root.tsx +14 -2
- package/src/components/ChessPuzzle/parts/__tests__/Hint.test.tsx +158 -0
- package/src/components/ChessPuzzle/parts/__tests__/PuzzleBoard.test.tsx +140 -0
- package/src/components/ChessPuzzle/parts/__tests__/Reset.test.tsx +341 -0
- package/src/components/ChessPuzzle/parts/__tests__/Root.test.tsx +42 -0
- package/src/hooks/__tests__/reducer.test.ts +134 -0
- package/src/hooks/reducer.ts +13 -1
- package/src/hooks/useChessPuzzle.ts +2 -0
- package/src/utils/__tests__/index.test.ts +0 -17
- package/src/utils/index.ts +1 -11
- package/README.MD +0 -344
|
@@ -1,46 +1,55 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
3
|
+
import { Status } from "../../../utils";
|
|
3
4
|
import { useChessPuzzleContext } from "../../..";
|
|
4
5
|
|
|
5
|
-
export interface HintProps
|
|
6
|
+
export interface HintProps extends Omit<
|
|
7
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
8
|
+
"onClick"
|
|
9
|
+
> {
|
|
6
10
|
asChild?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* The puzzle statuses in which the hint button should be visible.
|
|
13
|
+
* @default ["not-started", "in-progress"]
|
|
14
|
+
*/
|
|
7
15
|
showOn?: Status[];
|
|
8
16
|
}
|
|
9
17
|
|
|
10
18
|
const defaultShowOn: Status[] = ["not-started", "in-progress"];
|
|
11
19
|
|
|
12
|
-
export const Hint
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}) => {
|
|
20
|
+
export const Hint = React.forwardRef<
|
|
21
|
+
HTMLElement,
|
|
22
|
+
React.PropsWithChildren<HintProps>
|
|
23
|
+
>(({ children, asChild, showOn = defaultShowOn, className, ...rest }, ref) => {
|
|
17
24
|
const puzzleContext = useChessPuzzleContext();
|
|
18
25
|
if (!puzzleContext) {
|
|
19
26
|
throw new Error("PuzzleContext not found");
|
|
20
27
|
}
|
|
21
28
|
const { onHint, status } = puzzleContext;
|
|
22
|
-
|
|
29
|
+
|
|
30
|
+
const handleClick = React.useCallback(() => {
|
|
23
31
|
onHint();
|
|
24
|
-
};
|
|
32
|
+
}, [onHint]);
|
|
25
33
|
|
|
26
34
|
if (!showOn.includes(status)) {
|
|
27
35
|
return null;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<button type="button" onClick={handleClick}>
|
|
38
|
+
return asChild ? (
|
|
39
|
+
<Slot ref={ref} onClick={handleClick} className={className} {...rest}>
|
|
40
|
+
{children}
|
|
41
|
+
</Slot>
|
|
42
|
+
) : (
|
|
43
|
+
<button
|
|
44
|
+
ref={ref as React.RefObject<HTMLButtonElement>}
|
|
45
|
+
type="button"
|
|
46
|
+
className={className}
|
|
47
|
+
onClick={handleClick}
|
|
48
|
+
{...rest}
|
|
49
|
+
>
|
|
43
50
|
{children}
|
|
44
51
|
</button>
|
|
45
52
|
);
|
|
46
|
-
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
Hint.displayName = "ChessPuzzle.Hint";
|
|
@@ -12,34 +12,35 @@ export interface PuzzleBoardProps extends React.ComponentProps<
|
|
|
12
12
|
typeof ChessGame.Board
|
|
13
13
|
> {}
|
|
14
14
|
|
|
15
|
-
export const PuzzleBoard
|
|
16
|
-
options
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const gameContext = useChessGameContext();
|
|
21
|
-
const theme = useChessPuzzleTheme();
|
|
15
|
+
export const PuzzleBoard = React.forwardRef<HTMLDivElement, PuzzleBoardProps>(
|
|
16
|
+
({ options, ...rest }, ref) => {
|
|
17
|
+
const puzzleContext = useChessPuzzleContext();
|
|
18
|
+
const gameContext = useChessGameContext();
|
|
19
|
+
const theme = useChessPuzzleTheme();
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
if (!puzzleContext) {
|
|
22
|
+
throw new Error("PuzzleContext not found");
|
|
23
|
+
}
|
|
24
|
+
if (!gameContext) {
|
|
25
|
+
throw new Error("ChessGameContext not found");
|
|
26
|
+
}
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const { game } = gameContext;
|
|
29
|
+
const { status, hint, isPlayerTurn, nextMove } = puzzleContext;
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
31
|
+
const mergedOptions = deepMergeChessboardOptions(options || {}, {
|
|
32
|
+
squareStyles: getCustomSquareStyles(
|
|
33
|
+
status,
|
|
34
|
+
hint,
|
|
35
|
+
isPlayerTurn,
|
|
36
|
+
game,
|
|
37
|
+
stringToMove(game, nextMove),
|
|
38
|
+
theme,
|
|
39
|
+
),
|
|
40
|
+
});
|
|
43
41
|
|
|
44
|
-
|
|
45
|
-
}
|
|
42
|
+
return <ChessGame.Board ref={ref} {...rest} options={mergedOptions} />;
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
PuzzleBoard.displayName = "ChessPuzzle.PuzzleBoard";
|
|
@@ -1,51 +1,71 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
3
|
+
import { type Puzzle, type Status } from "../../../utils";
|
|
3
4
|
import { useChessPuzzleContext, type ChessPuzzleContextType } from "../../..";
|
|
4
5
|
|
|
5
|
-
export interface ResetProps
|
|
6
|
+
export interface ResetProps extends Omit<
|
|
7
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
8
|
+
"onReset"
|
|
9
|
+
> {
|
|
6
10
|
asChild?: boolean;
|
|
7
11
|
puzzle?: Puzzle;
|
|
8
12
|
onReset?: (puzzleContext: ChessPuzzleContextType) => void;
|
|
13
|
+
/**
|
|
14
|
+
* The puzzle statuses in which the reset button should be visible.
|
|
15
|
+
* @default ["failed", "solved"]
|
|
16
|
+
*/
|
|
9
17
|
showOn?: Status[];
|
|
10
18
|
}
|
|
11
19
|
|
|
12
20
|
const defaultShowOn: Status[] = ["failed", "solved"];
|
|
13
21
|
|
|
14
|
-
export const Reset
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
export const Reset = React.forwardRef<
|
|
23
|
+
HTMLElement,
|
|
24
|
+
React.PropsWithChildren<ResetProps>
|
|
25
|
+
>(
|
|
26
|
+
(
|
|
27
|
+
{
|
|
28
|
+
children,
|
|
29
|
+
asChild,
|
|
30
|
+
puzzle,
|
|
31
|
+
onReset,
|
|
32
|
+
showOn = defaultShowOn,
|
|
33
|
+
className,
|
|
34
|
+
...rest
|
|
35
|
+
},
|
|
36
|
+
ref,
|
|
37
|
+
) => {
|
|
38
|
+
const puzzleContext = useChessPuzzleContext();
|
|
39
|
+
if (!puzzleContext) {
|
|
40
|
+
throw new Error("PuzzleContext not found");
|
|
41
|
+
}
|
|
42
|
+
const { changePuzzle, puzzle: contextPuzzle, status } = puzzleContext;
|
|
30
43
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
const handleClick = React.useCallback(() => {
|
|
45
|
+
changePuzzle(puzzle || contextPuzzle);
|
|
46
|
+
onReset?.(puzzleContext);
|
|
47
|
+
}, [changePuzzle, puzzle, contextPuzzle, puzzleContext, onReset]);
|
|
34
48
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (isClickableElement(child)) {
|
|
38
|
-
return React.cloneElement(child, {
|
|
39
|
-
onClick: handleClick,
|
|
40
|
-
});
|
|
41
|
-
} else {
|
|
42
|
-
throw new Error("Change child must be a clickable element");
|
|
49
|
+
if (!showOn.includes(status)) {
|
|
50
|
+
return null;
|
|
43
51
|
}
|
|
44
|
-
}
|
|
45
52
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
return asChild ? (
|
|
54
|
+
<Slot ref={ref} onClick={handleClick} className={className} {...rest}>
|
|
55
|
+
{children}
|
|
56
|
+
</Slot>
|
|
57
|
+
) : (
|
|
58
|
+
<button
|
|
59
|
+
ref={ref as React.RefObject<HTMLButtonElement>}
|
|
60
|
+
type="button"
|
|
61
|
+
className={className}
|
|
62
|
+
onClick={handleClick}
|
|
63
|
+
{...rest}
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
</button>
|
|
67
|
+
);
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
Reset.displayName = "ChessPuzzle.Reset";
|
|
@@ -16,12 +16,15 @@ export interface RootProps {
|
|
|
16
16
|
onFail?: (puzzleContext: ChessPuzzleContextType) => void;
|
|
17
17
|
/** Optional theme configuration. Supports partial themes - only override the colors you need. */
|
|
18
18
|
theme?: PartialChessPuzzleTheme;
|
|
19
|
+
/** When true, any checkmate move solves the puzzle (not just the canonical solution). Defaults to true. */
|
|
20
|
+
solveOnCheckmate?: boolean;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
interface PuzzleRootInnerProps {
|
|
22
24
|
puzzle: Puzzle;
|
|
23
25
|
onSolve?: (puzzleContext: ChessPuzzleContextType) => void;
|
|
24
26
|
onFail?: (puzzleContext: ChessPuzzleContextType) => void;
|
|
27
|
+
solveOnCheckmate: boolean;
|
|
25
28
|
children: React.ReactNode;
|
|
26
29
|
}
|
|
27
30
|
|
|
@@ -29,9 +32,10 @@ const PuzzleRootInner: React.FC<PuzzleRootInnerProps> = ({
|
|
|
29
32
|
puzzle,
|
|
30
33
|
onSolve,
|
|
31
34
|
onFail,
|
|
35
|
+
solveOnCheckmate,
|
|
32
36
|
children,
|
|
33
37
|
}) => {
|
|
34
|
-
const context = useChessPuzzle(puzzle, onSolve, onFail);
|
|
38
|
+
const context = useChessPuzzle(puzzle, onSolve, onFail, solveOnCheckmate);
|
|
35
39
|
|
|
36
40
|
return (
|
|
37
41
|
<ChessPuzzleContext.Provider value={context}>
|
|
@@ -45,6 +49,7 @@ export const Root: React.FC<React.PropsWithChildren<RootProps>> = ({
|
|
|
45
49
|
onSolve,
|
|
46
50
|
onFail,
|
|
47
51
|
theme,
|
|
52
|
+
solveOnCheckmate = true,
|
|
48
53
|
children,
|
|
49
54
|
}) => {
|
|
50
55
|
// Merge partial theme with defaults
|
|
@@ -57,10 +62,17 @@ export const Root: React.FC<React.PropsWithChildren<RootProps>> = ({
|
|
|
57
62
|
theme={mergedTheme}
|
|
58
63
|
>
|
|
59
64
|
<PuzzleThemeProvider theme={mergedTheme}>
|
|
60
|
-
<PuzzleRootInner
|
|
65
|
+
<PuzzleRootInner
|
|
66
|
+
puzzle={puzzle}
|
|
67
|
+
onSolve={onSolve}
|
|
68
|
+
onFail={onFail}
|
|
69
|
+
solveOnCheckmate={solveOnCheckmate}
|
|
70
|
+
>
|
|
61
71
|
{children}
|
|
62
72
|
</PuzzleRootInner>
|
|
63
73
|
</PuzzleThemeProvider>
|
|
64
74
|
</ChessGame.Root>
|
|
65
75
|
);
|
|
66
76
|
};
|
|
77
|
+
|
|
78
|
+
Root.displayName = "ChessPuzzle.Root";
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import "@testing-library/jest-dom";
|
|
4
|
+
import { ChessPuzzle } from "../..";
|
|
5
|
+
import { Hint } from "../Hint";
|
|
6
|
+
import { Puzzle } from "../../../../utils";
|
|
7
|
+
|
|
8
|
+
describe("ChessPuzzle.Hint", () => {
|
|
9
|
+
const mockPuzzle: Puzzle = {
|
|
10
|
+
fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
|
|
11
|
+
moves: ["e4", "e5"],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
it("should have correct displayName", () => {
|
|
15
|
+
expect(Hint.displayName).toBe("ChessPuzzle.Hint");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should forward ref to button element", () => {
|
|
19
|
+
const ref = React.createRef<HTMLElement>();
|
|
20
|
+
|
|
21
|
+
render(
|
|
22
|
+
<ChessPuzzle.Root puzzle={mockPuzzle}>
|
|
23
|
+
<Hint ref={ref} />
|
|
24
|
+
</ChessPuzzle.Root>,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should forward ref when using asChild", () => {
|
|
31
|
+
const ref = React.createRef<HTMLElement>();
|
|
32
|
+
|
|
33
|
+
render(
|
|
34
|
+
<ChessPuzzle.Root puzzle={mockPuzzle}>
|
|
35
|
+
<Hint ref={ref} asChild>
|
|
36
|
+
<button>Custom Hint</button>
|
|
37
|
+
</Hint>
|
|
38
|
+
</ChessPuzzle.Root>,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should be visible when status matches showOn", () => {
|
|
45
|
+
render(
|
|
46
|
+
<ChessPuzzle.Root puzzle={mockPuzzle}>
|
|
47
|
+
<Hint showOn={["not-started"]}>Hint</Hint>
|
|
48
|
+
</ChessPuzzle.Root>,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(screen.getByRole("button")).toBeInTheDocument();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should be hidden when status does not match showOn", () => {
|
|
55
|
+
render(
|
|
56
|
+
<ChessPuzzle.Root puzzle={mockPuzzle}>
|
|
57
|
+
<Hint showOn={["solved"]}>Hint</Hint>
|
|
58
|
+
</ChessPuzzle.Root>,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(screen.queryByRole("button")).not.toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should default to showing on not-started and in-progress statuses", () => {
|
|
65
|
+
render(
|
|
66
|
+
<ChessPuzzle.Root puzzle={mockPuzzle}>
|
|
67
|
+
<Hint>Hint</Hint>
|
|
68
|
+
</ChessPuzzle.Root>,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(screen.getByRole("button")).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should accept multiple statuses in showOn", () => {
|
|
75
|
+
render(
|
|
76
|
+
<ChessPuzzle.Root puzzle={mockPuzzle}>
|
|
77
|
+
<Hint showOn={["not-started", "in-progress", "failed", "solved"]}>
|
|
78
|
+
Hint
|
|
79
|
+
</Hint>
|
|
80
|
+
</ChessPuzzle.Root>,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(screen.getByRole("button")).toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should render custom element when asChild is true", () => {
|
|
87
|
+
render(
|
|
88
|
+
<ChessPuzzle.Root puzzle={mockPuzzle}>
|
|
89
|
+
<Hint asChild>
|
|
90
|
+
<button className="custom-button">Custom Hint</button>
|
|
91
|
+
</Hint>
|
|
92
|
+
</ChessPuzzle.Root>,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const button = screen.getByRole("button");
|
|
96
|
+
expect(button).toHaveTextContent("Custom Hint");
|
|
97
|
+
expect(button).toHaveClass("custom-button");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should compose onClick handlers with asChild", () => {
|
|
101
|
+
const childOnClick = jest.fn();
|
|
102
|
+
|
|
103
|
+
render(
|
|
104
|
+
<ChessPuzzle.Root puzzle={mockPuzzle}>
|
|
105
|
+
<Hint asChild>
|
|
106
|
+
<button onClick={childOnClick}>Hint</button>
|
|
107
|
+
</Hint>
|
|
108
|
+
</ChessPuzzle.Root>,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
fireEvent.click(screen.getByRole("button"));
|
|
112
|
+
expect(childOnClick).toHaveBeenCalledTimes(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should throw error when used outside ChessPuzzle.Root", () => {
|
|
116
|
+
const consoleError = jest
|
|
117
|
+
.spyOn(console, "error")
|
|
118
|
+
.mockImplementation(() => {});
|
|
119
|
+
|
|
120
|
+
expect(() => {
|
|
121
|
+
render(<Hint>Hint</Hint>);
|
|
122
|
+
}).toThrow();
|
|
123
|
+
|
|
124
|
+
consoleError.mockRestore();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should render text children", () => {
|
|
128
|
+
render(
|
|
129
|
+
<ChessPuzzle.Root puzzle={mockPuzzle}>
|
|
130
|
+
<Hint>Show Hint</Hint>
|
|
131
|
+
</ChessPuzzle.Root>,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
expect(screen.getByRole("button")).toHaveTextContent("Show Hint");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should render element children", () => {
|
|
138
|
+
render(
|
|
139
|
+
<ChessPuzzle.Root puzzle={mockPuzzle}>
|
|
140
|
+
<Hint>
|
|
141
|
+
<span data-testid="child">Hint Icon</span>
|
|
142
|
+
</Hint>
|
|
143
|
+
</ChessPuzzle.Root>,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(screen.getByTestId("child")).toBeInTheDocument();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should render without children", () => {
|
|
150
|
+
render(
|
|
151
|
+
<ChessPuzzle.Root puzzle={mockPuzzle}>
|
|
152
|
+
<Hint aria-label="Show hint" />
|
|
153
|
+
</ChessPuzzle.Root>,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
expect(screen.getByRole("button")).toBeInTheDocument();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import "@testing-library/jest-dom";
|
|
4
|
+
import { ChessPuzzle } from "../..";
|
|
5
|
+
import { PuzzleBoard } from "../PuzzleBoard";
|
|
6
|
+
import { Puzzle } from "../../../../utils";
|
|
7
|
+
import { ChessGame } from "@react-chess-tools/react-chess-game";
|
|
8
|
+
|
|
9
|
+
describe("ChessPuzzle.PuzzleBoard", () => {
|
|
10
|
+
const mockPuzzle: Puzzle = {
|
|
11
|
+
fen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
|
|
12
|
+
moves: ["e4", "e5"],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
it("should have correct displayName", () => {
|
|
16
|
+
expect(PuzzleBoard.displayName).toBe("ChessPuzzle.PuzzleBoard");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should forward ref to underlying Board component", () => {
|
|
20
|
+
const ref = React.createRef<HTMLDivElement>();
|
|
21
|
+
|
|
22
|
+
render(
|
|
23
|
+
<ChessPuzzle.Root puzzle={mockPuzzle}>
|
|
24
|
+
<PuzzleBoard ref={ref} />
|
|
25
|
+
</ChessPuzzle.Root>,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
expect(ref.current).toBeInstanceOf(HTMLDivElement);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should allow focusing via ref", () => {
|
|
32
|
+
const ref = React.createRef<HTMLDivElement>();
|
|
33
|
+
|
|
34
|
+
render(
|
|
35
|
+
<ChessPuzzle.Root puzzle={mockPuzzle}>
|
|
36
|
+
<PuzzleBoard ref={ref} tabIndex={0} />
|
|
37
|
+
</ChessPuzzle.Root>,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
expect(ref.current).toBeInstanceOf(HTMLDivElement);
|
|
41
|
+
|
|
42
|
+
// Note: focus() doesn't work in JSDOM, but we can verify the ref points to the element
|
|
43
|
+
// In a real browser, ref.current?.focus() would set document.activeElement to ref.current
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should apply custom className", () => {
|
|
47
|
+
const { container } = render(
|
|
48
|
+
<ChessPuzzle.Root puzzle={mockPuzzle}>
|
|
49
|
+
<PuzzleBoard className="custom-puzzle-board-class" />
|
|
50
|
+
</ChessPuzzle.Root>,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const board = container.querySelector('[style*="position: relative"]');
|
|
54
|
+
expect(board).toHaveClass("custom-puzzle-board-class");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should apply custom style", () => {
|
|
58
|
+
const customStyle = { border: "2px solid blue", margin: "15px" };
|
|
59
|
+
|
|
60
|
+
const { container } = render(
|
|
61
|
+
<ChessPuzzle.Root puzzle={mockPuzzle}>
|
|
62
|
+
<PuzzleBoard style={customStyle} />
|
|
63
|
+
</ChessPuzzle.Root>,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const board = container.querySelector('[style*="position: relative"]');
|
|
67
|
+
expect(board).toHaveStyle({ border: "2px solid blue" });
|
|
68
|
+
expect(board).toHaveStyle({ margin: "15px" });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should apply custom id", () => {
|
|
72
|
+
const { container } = render(
|
|
73
|
+
<ChessPuzzle.Root puzzle={mockPuzzle}>
|
|
74
|
+
<PuzzleBoard id="custom-puzzle-board-id" />
|
|
75
|
+
</ChessPuzzle.Root>,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const board = container.querySelector('[style*="position: relative"]');
|
|
79
|
+
expect(board).toHaveAttribute("id", "custom-puzzle-board-id");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should apply data-* attributes", () => {
|
|
83
|
+
render(
|
|
84
|
+
<ChessPuzzle.Root puzzle={mockPuzzle}>
|
|
85
|
+
<PuzzleBoard data-testid="puzzle-board" data-custom="puzzle-value" />
|
|
86
|
+
</ChessPuzzle.Root>,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const board = screen.getByTestId("puzzle-board");
|
|
90
|
+
expect(board).toHaveAttribute("data-custom", "puzzle-value");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should apply aria-* attributes", () => {
|
|
94
|
+
const { container } = render(
|
|
95
|
+
<ChessPuzzle.Root puzzle={mockPuzzle}>
|
|
96
|
+
<PuzzleBoard aria-label="Puzzle board" aria-describedby="puzzle-desc" />
|
|
97
|
+
</ChessPuzzle.Root>,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const board = container.querySelector('[style*="position: relative"]');
|
|
101
|
+
expect(board).toHaveAttribute("aria-label", "Puzzle board");
|
|
102
|
+
expect(board).toHaveAttribute("aria-describedby", "puzzle-desc");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should accept custom onClick handler", () => {
|
|
106
|
+
const handleClick = jest.fn();
|
|
107
|
+
|
|
108
|
+
const { container } = render(
|
|
109
|
+
<ChessPuzzle.Root puzzle={mockPuzzle}>
|
|
110
|
+
<PuzzleBoard onClick={handleClick} />
|
|
111
|
+
</ChessPuzzle.Root>,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const board = container.querySelector(
|
|
115
|
+
'[style*="position: relative"]',
|
|
116
|
+
) as HTMLElement;
|
|
117
|
+
board?.click();
|
|
118
|
+
|
|
119
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should throw error when used outside ChessPuzzle.Root", () => {
|
|
123
|
+
// Suppress console.error for this test
|
|
124
|
+
const consoleError = jest
|
|
125
|
+
.spyOn(console, "error")
|
|
126
|
+
.mockImplementation(() => {});
|
|
127
|
+
|
|
128
|
+
expect(() => {
|
|
129
|
+
render(
|
|
130
|
+
<ChessGame.Root>
|
|
131
|
+
<PuzzleBoard />
|
|
132
|
+
</ChessGame.Root>,
|
|
133
|
+
);
|
|
134
|
+
}).toThrow(
|
|
135
|
+
"useChessPuzzleContext must be used within a ChessPuzzle component",
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
consoleError.mockRestore();
|
|
139
|
+
});
|
|
140
|
+
});
|