UNPKG

@react-chess-tools/react-chess-game

Version:

react-chess-game is a React component bridging chess.js with react-chessboard to offer a full-featured, ready-to-integrate chess board experience.

517 lines (435 loc) 16.2 kB
import { Chess } from "chess.js"; import { getCustomSquareStyles, deepMergeChessboardOptions } from "../board"; import { getGameInfo } from "../chess"; import { defaultGameTheme } from "../../theme/defaults"; import type { ChessGameTheme } from "../../theme/types"; describe("Board Utilities", () => { describe("getCustomSquareStyles", () => { it("should return empty styles for initial position with no active square", () => { const game = new Chess(); const orientation = "w"; const info = getGameInfo(game, orientation); const activeSquare = null; const styles = getCustomSquareStyles(game, info, activeSquare); expect(Object.keys(styles).length).toBe(0); }); it("should highlight last move squares", () => { const game = new Chess(); game.move("e4"); game.move("e5"); const orientation = "w"; const info = getGameInfo(game, orientation); const activeSquare = null; const styles = getCustomSquareStyles(game, info, activeSquare); expect(styles).toHaveProperty("e7"); expect(styles).toHaveProperty("e5"); expect(styles.e7).toHaveProperty( "backgroundColor", "rgba(255, 255, 0, 0.5)", ); expect(styles.e5).toHaveProperty( "backgroundColor", "rgba(255, 255, 0, 0.5)", ); }); it("should highlight active square", () => { const game = new Chess(); const orientation = "w"; const info = getGameInfo(game, orientation); const activeSquare = "e2"; const styles = getCustomSquareStyles(game, info, activeSquare); expect(styles).toHaveProperty("e2"); expect(styles.e2).toHaveProperty( "backgroundColor", "rgba(255, 255, 0, 0.5)", ); }); it("should highlight destination squares for active square", () => { const game = new Chess(); const orientation = "w"; const info = getGameInfo(game, orientation); const activeSquare = "e2"; const styles = getCustomSquareStyles(game, info, activeSquare); expect(styles).toHaveProperty("e3"); expect(styles).toHaveProperty("e4"); expect(styles.e3).toHaveProperty("background"); expect(styles.e4).toHaveProperty("background"); }); it("should highlight destination squares with captures differently", () => { const game = new Chess( "rnbqkbnr/ppp1pppp/8/3p4/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2", ); const orientation = "w"; const info = getGameInfo(game, orientation); const activeSquare = "e4"; const styles = getCustomSquareStyles(game, info, activeSquare); expect(styles).toHaveProperty("d5"); expect(styles.d5.background).toContain("radial-gradient"); expect(styles.d5.background).toContain("85%"); }); it("should highlight king in check", () => { // Set up a position where the king is in check const game = new Chess( "rnbqkbnr/ppp2ppp/8/3pp3/4P3/5Q2/PPPP1PPP/RNB1KBNR b KQkq - 1 3", ); // We need to manually set the isCheck flag in the info object const orientation = "b"; const info = getGameInfo(game, orientation); const modifiedInfo = { ...info, isCheck: true }; const activeSquare = null; const styles = getCustomSquareStyles(game, modifiedInfo, activeSquare); // Black king on e8 should be highlighted as in check expect(styles).toHaveProperty("e8"); expect(styles.e8).toHaveProperty( "backgroundColor", "rgba(255, 0, 0, 0.5)", ); }); it("should combine multiple style effects", () => { const game = new Chess( "rnbqkbnr/ppp2ppp/8/3pp3/4P3/5Q2/PPPP1PPP/RNB1KBNR b KQkq - 1 3", ); game.move("Ke7"); const orientation = "w"; const info = getGameInfo(game, orientation); const activeSquare = "f3"; const styles = getCustomSquareStyles(game, info, activeSquare); expect(styles).toHaveProperty("f3"); expect(styles.f3).toHaveProperty( "backgroundColor", "rgba(255, 255, 0, 0.5)", ); expect(styles).toHaveProperty("e8"); expect(styles).toHaveProperty("e7"); expect(Object.keys(styles).length).toBeGreaterThan(3); }); it("should highlight empty destination squares differently from capture squares", () => { const game = new Chess(); const orientation = "w"; const info = getGameInfo(game, orientation); const activeSquare = "b1"; const styles = getCustomSquareStyles(game, info, activeSquare); expect(styles).toHaveProperty("a3"); expect(styles).toHaveProperty("c3"); expect(styles.a3.background).toContain("25%"); expect(styles.c3.background).toContain("25%"); }); describe("with custom theme", () => { const customTheme: ChessGameTheme = { ...defaultGameTheme, state: { ...defaultGameTheme.state, lastMove: "rgba(100, 200, 100, 0.6)", check: "rgba(200, 50, 50, 0.7)", activeSquare: "rgba(100, 100, 255, 0.5)", }, indicators: { move: "rgba(50, 50, 50, 0.2)", capture: "rgba(200, 50, 50, 0.3)", }, }; it("should use custom theme colors for last move", () => { const game = new Chess(); game.move("e4"); const orientation = "w"; const info = getGameInfo(game, orientation); const activeSquare = null; const styles = getCustomSquareStyles( game, info, activeSquare, customTheme, ); expect(styles.e2).toHaveProperty( "backgroundColor", "rgba(100, 200, 100, 0.6)", ); expect(styles.e4).toHaveProperty( "backgroundColor", "rgba(100, 200, 100, 0.6)", ); }); it("should use custom theme colors for active square", () => { const game = new Chess(); const orientation = "w"; const info = getGameInfo(game, orientation); const activeSquare = "e2"; const styles = getCustomSquareStyles( game, info, activeSquare, customTheme, ); expect(styles.e2).toHaveProperty( "backgroundColor", "rgba(100, 100, 255, 0.5)", ); }); it("should use custom theme colors for check", () => { const game = new Chess( "rnbqkbnr/ppp2ppp/8/3pp3/4P3/5Q2/PPPP1PPP/RNB1KBNR b KQkq - 1 3", ); const orientation = "b"; const info = getGameInfo(game, orientation); const modifiedInfo = { ...info, isCheck: true }; const activeSquare = null; const styles = getCustomSquareStyles( game, modifiedInfo, activeSquare, customTheme, ); expect(styles.e8).toHaveProperty( "backgroundColor", "rgba(200, 50, 50, 0.7)", ); }); it("should use custom theme colors for move indicators", () => { const game = new Chess(); const orientation = "w"; const info = getGameInfo(game, orientation); const activeSquare = "e2"; const styles = getCustomSquareStyles( game, info, activeSquare, customTheme, ); expect(styles.e3.background).toContain("rgba(50, 50, 50, 0.2)"); expect(styles.e4.background).toContain("rgba(50, 50, 50, 0.2)"); }); it("should use custom theme colors for capture indicators", () => { const game = new Chess( "rnbqkbnr/ppp1pppp/8/3p4/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2", ); const orientation = "w"; const info = getGameInfo(game, orientation); const activeSquare = "e4"; const styles = getCustomSquareStyles( game, info, activeSquare, customTheme, ); expect(styles.d5.background).toContain("rgba(200, 50, 50, 0.3)"); }); }); }); describe("deepMergeChessboardOptions", () => { it("should deeply merge nested object options without overriding computed values", () => { const baseOptions = { squareStyles: { e4: { backgroundColor: "rgba(255, 255, 0, 0.5)", // Computed move highlighting background: "radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)", // Move dot }, }, dropSquareStyle: { backgroundColor: "rgba(255, 255, 0, 0.4)", border: "2px dashed yellow", }, showNotation: true, allowDrawingArrows: true, }; const customOptions = { squareStyles: { e4: { border: "2px solid red", // Should be added without removing background }, a1: { backgroundColor: "blue", // New square style }, }, dropSquareStyle: { backgroundColor: "rgba(0, 255, 0, 0.6)", // Should override backgroundColor but preserve border }, showNotation: false, // Should override primitive }; const result = deepMergeChessboardOptions(baseOptions, customOptions); // squareStyles should deep merge expect(result.squareStyles?.e4).toEqual({ backgroundColor: "rgba(255, 255, 0, 0.5)", background: "radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)", border: "2px solid red", }); expect(result.squareStyles?.a1).toEqual({ backgroundColor: "blue", }); // dropSquareStyle should deep merge expect(result.dropSquareStyle).toEqual({ backgroundColor: "rgba(0, 255, 0, 0.6)", border: "2px dashed yellow", }); // Primitives should override expect(result.showNotation).toBe(false); expect(result.allowDrawingArrows).toBe(true); }); it("should handle function properties by overwriting them", () => { const baseOnSquareClick = jest.fn(); const baseCanDragPiece = jest.fn(); const customOnSquareClick = jest.fn(); const baseOptions = { onSquareClick: baseOnSquareClick, canDragPiece: baseCanDragPiece, showNotation: true, }; const customOptions = { onSquareClick: customOnSquareClick, // canDragPiece not provided - should keep base function }; const result = deepMergeChessboardOptions(baseOptions, customOptions); // Custom function should replace base function expect(result.onSquareClick).toBe(customOnSquareClick); expect(result.onSquareClick).not.toBe(baseOnSquareClick); // Base function should be preserved when not overridden expect(result.canDragPiece).toBe(baseCanDragPiece); // Other properties should merge normally expect(result.showNotation).toBe(true); }); it("should handle nested object properties by deep merging them", () => { const baseOptions = { darkSquareStyle: { backgroundColor: "#8B4513", }, }; const customOptions = { darkSquareStyle: { backgroundColor: "#654321", border: "1px solid black", }, }; const result = deepMergeChessboardOptions(baseOptions, customOptions); expect(result.darkSquareStyle).toEqual({ backgroundColor: "#654321", border: "1px solid black", }); }); it("should handle undefined custom options gracefully", () => { const baseOptions = { squareStyles: { e4: { backgroundColor: "yellow" }, }, showNotation: true, }; const result = deepMergeChessboardOptions(baseOptions, undefined); expect(result).toEqual(baseOptions); expect(result).not.toBe(baseOptions); // Should be a new object }); it("should handle empty custom options", () => { const baseOptions = { squareStyles: { e4: { backgroundColor: "yellow" }, }, showNotation: true, }; const result = deepMergeChessboardOptions(baseOptions, {}); expect(result).toEqual(baseOptions); expect(result).not.toBe(baseOptions); // Should be a new object }); it("should preserve complex nested object structures", () => { const baseOptions = { squareStyles: { e4: { backgroundColor: "rgba(255, 255, 0, 0.5)", background: "radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)", border: "1px solid black", }, d5: { backgroundColor: "rgba(0, 255, 0, 0.3)", }, }, }; const customOptions = { squareStyles: { e4: { borderRadius: "4px", // Add new property backgroundColor: "rgba(255, 0, 0, 0.5)", // Override existing // background should be preserved from base }, f6: { backgroundColor: "blue", // New square }, }, }; const result = deepMergeChessboardOptions(baseOptions, customOptions); expect(result.squareStyles?.e4).toEqual({ backgroundColor: "rgba(255, 0, 0, 0.5)", // Overridden background: "radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)", // Preserved border: "1px solid black", // Preserved borderRadius: "4px", // Added }); expect(result.squareStyles?.d5).toEqual({ backgroundColor: "rgba(0, 255, 0, 0.3)", // Preserved from base }); expect(result.squareStyles?.f6).toEqual({ backgroundColor: "blue", // Added from custom }); }); it("should handle real-world Board component use case", () => { // Simulate the actual use case in Board component const computedSquareStyles = { e2: { backgroundColor: "rgba(255, 255, 0, 0.5)", // Active square highlight }, e3: { background: "radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)", // Move dot }, e4: { background: "radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)", // Move dot }, }; const baseOptions = { squareStyles: computedSquareStyles, boardOrientation: "white" as const, position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", showNotation: true, onSquareClick: jest.fn(), }; const userCustomOptions = { squareStyles: { e4: { border: "2px solid red", // User wants to add border to a square that has move dots }, a1: { backgroundColor: "lightblue", // User wants to highlight corner square }, }, showNotation: false, // User wants to hide notation onSquareClick: jest.fn(), // User provides custom click handler }; const result = deepMergeChessboardOptions(baseOptions, userCustomOptions); // Move highlighting should be preserved while adding user's border expect(result.squareStyles?.e4).toEqual({ background: "radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)", border: "2px solid red", }); // User's new square style should be added expect(result.squareStyles?.a1).toEqual({ backgroundColor: "lightblue", }); // Other computed styles should be preserved expect(result.squareStyles?.e2).toEqual({ backgroundColor: "rgba(255, 255, 0, 0.5)", }); expect(result.squareStyles?.e3).toEqual({ background: "radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)", }); // Primitives should be overridden expect(result.showNotation).toBe(false); expect(result.position).toBe( "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", ); // Functions should be replaced expect(result.onSquareClick).toBe(userCustomOptions.onSquareClick); expect(result.onSquareClick).not.toBe(baseOptions.onSquareClick); }); }); });