@react-chess-tools/react-chess-game 0.4.2 → 0.5.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.
@@ -1,5 +1,5 @@
1
1
  import { Chess } from "chess.js";
2
- import { getCustomSquareStyles } from "../board";
2
+ import { getCustomSquareStyles, deepMergeChessboardOptions } from "../board";
3
3
  import { getGameInfo } from "../chess";
4
4
 
5
5
  describe("Board Utilities", () => {
@@ -140,4 +140,259 @@ describe("Board Utilities", () => {
140
140
  expect(styles.c3.background).toContain("25%");
141
141
  });
142
142
  });
143
+
144
+ describe("deepMergeChessboardOptions", () => {
145
+ it("should deeply merge nested object options without overriding computed values", () => {
146
+ const baseOptions = {
147
+ squareStyles: {
148
+ e4: {
149
+ backgroundColor: "rgba(255, 255, 0, 0.5)", // Computed move highlighting
150
+ background:
151
+ "radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)", // Move dot
152
+ },
153
+ },
154
+ dropSquareStyle: {
155
+ backgroundColor: "rgba(255, 255, 0, 0.4)",
156
+ border: "2px dashed yellow",
157
+ },
158
+ showNotation: true,
159
+ allowDrawingArrows: true,
160
+ };
161
+
162
+ const customOptions = {
163
+ squareStyles: {
164
+ e4: {
165
+ border: "2px solid red", // Should be added without removing background
166
+ },
167
+ a1: {
168
+ backgroundColor: "blue", // New square style
169
+ },
170
+ },
171
+ dropSquareStyle: {
172
+ backgroundColor: "rgba(0, 255, 0, 0.6)", // Should override backgroundColor but preserve border
173
+ },
174
+ showNotation: false, // Should override primitive
175
+ };
176
+
177
+ const result = deepMergeChessboardOptions(baseOptions, customOptions);
178
+
179
+ // squareStyles should deep merge
180
+ expect(result.squareStyles?.e4).toEqual({
181
+ backgroundColor: "rgba(255, 255, 0, 0.5)",
182
+ background:
183
+ "radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)",
184
+ border: "2px solid red",
185
+ });
186
+ expect(result.squareStyles?.a1).toEqual({
187
+ backgroundColor: "blue",
188
+ });
189
+
190
+ // dropSquareStyle should deep merge
191
+ expect(result.dropSquareStyle).toEqual({
192
+ backgroundColor: "rgba(0, 255, 0, 0.6)",
193
+ border: "2px dashed yellow",
194
+ });
195
+
196
+ // Primitives should override
197
+ expect(result.showNotation).toBe(false);
198
+ expect(result.allowDrawingArrows).toBe(true);
199
+ });
200
+
201
+ it("should handle function properties by overwriting them", () => {
202
+ const baseOnSquareClick = jest.fn();
203
+ const baseCanDragPiece = jest.fn();
204
+ const customOnSquareClick = jest.fn();
205
+
206
+ const baseOptions = {
207
+ onSquareClick: baseOnSquareClick,
208
+ canDragPiece: baseCanDragPiece,
209
+ showNotation: true,
210
+ };
211
+
212
+ const customOptions = {
213
+ onSquareClick: customOnSquareClick,
214
+ // canDragPiece not provided - should keep base function
215
+ };
216
+
217
+ const result = deepMergeChessboardOptions(baseOptions, customOptions);
218
+
219
+ // Custom function should replace base function
220
+ expect(result.onSquareClick).toBe(customOnSquareClick);
221
+ expect(result.onSquareClick).not.toBe(baseOnSquareClick);
222
+
223
+ // Base function should be preserved when not overridden
224
+ expect(result.canDragPiece).toBe(baseCanDragPiece);
225
+
226
+ // Other properties should merge normally
227
+ expect(result.showNotation).toBe(true);
228
+ });
229
+
230
+ it("should handle nested object properties by deep merging them", () => {
231
+ const baseOptions = {
232
+ darkSquareStyle: {
233
+ backgroundColor: "#8B4513",
234
+ },
235
+ };
236
+
237
+ const customOptions = {
238
+ darkSquareStyle: {
239
+ backgroundColor: "#654321",
240
+ border: "1px solid black",
241
+ },
242
+ };
243
+
244
+ const result = deepMergeChessboardOptions(baseOptions, customOptions);
245
+
246
+ expect(result.darkSquareStyle).toEqual({
247
+ backgroundColor: "#654321",
248
+ border: "1px solid black",
249
+ });
250
+ });
251
+
252
+ it("should handle undefined custom options gracefully", () => {
253
+ const baseOptions = {
254
+ squareStyles: {
255
+ e4: { backgroundColor: "yellow" },
256
+ },
257
+ showNotation: true,
258
+ };
259
+
260
+ const result = deepMergeChessboardOptions(baseOptions, undefined);
261
+
262
+ expect(result).toEqual(baseOptions);
263
+ expect(result).not.toBe(baseOptions); // Should be a new object
264
+ });
265
+
266
+ it("should handle empty custom options", () => {
267
+ const baseOptions = {
268
+ squareStyles: {
269
+ e4: { backgroundColor: "yellow" },
270
+ },
271
+ showNotation: true,
272
+ };
273
+
274
+ const result = deepMergeChessboardOptions(baseOptions, {});
275
+
276
+ expect(result).toEqual(baseOptions);
277
+ expect(result).not.toBe(baseOptions); // Should be a new object
278
+ });
279
+
280
+ it("should preserve complex nested object structures", () => {
281
+ const baseOptions = {
282
+ squareStyles: {
283
+ e4: {
284
+ backgroundColor: "rgba(255, 255, 0, 0.5)",
285
+ background:
286
+ "radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)",
287
+ border: "1px solid black",
288
+ },
289
+ d5: {
290
+ backgroundColor: "rgba(0, 255, 0, 0.3)",
291
+ },
292
+ },
293
+ };
294
+
295
+ const customOptions = {
296
+ squareStyles: {
297
+ e4: {
298
+ borderRadius: "4px", // Add new property
299
+ backgroundColor: "rgba(255, 0, 0, 0.5)", // Override existing
300
+ // background should be preserved from base
301
+ },
302
+ f6: {
303
+ backgroundColor: "blue", // New square
304
+ },
305
+ },
306
+ };
307
+
308
+ const result = deepMergeChessboardOptions(baseOptions, customOptions);
309
+
310
+ expect(result.squareStyles?.e4).toEqual({
311
+ backgroundColor: "rgba(255, 0, 0, 0.5)", // Overridden
312
+ background:
313
+ "radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)", // Preserved
314
+ border: "1px solid black", // Preserved
315
+ borderRadius: "4px", // Added
316
+ });
317
+
318
+ expect(result.squareStyles?.d5).toEqual({
319
+ backgroundColor: "rgba(0, 255, 0, 0.3)", // Preserved from base
320
+ });
321
+
322
+ expect(result.squareStyles?.f6).toEqual({
323
+ backgroundColor: "blue", // Added from custom
324
+ });
325
+ });
326
+
327
+ it("should handle real-world Board component use case", () => {
328
+ // Simulate the actual use case in Board component
329
+ const computedSquareStyles = {
330
+ e2: {
331
+ backgroundColor: "rgba(255, 255, 0, 0.5)", // Active square highlight
332
+ },
333
+ e3: {
334
+ background:
335
+ "radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)", // Move dot
336
+ },
337
+ e4: {
338
+ background:
339
+ "radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)", // Move dot
340
+ },
341
+ };
342
+
343
+ const baseOptions = {
344
+ squareStyles: computedSquareStyles,
345
+ boardOrientation: "white" as const,
346
+ position: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
347
+ showNotation: true,
348
+ onSquareClick: jest.fn(),
349
+ };
350
+
351
+ const userCustomOptions = {
352
+ squareStyles: {
353
+ e4: {
354
+ border: "2px solid red", // User wants to add border to a square that has move dots
355
+ },
356
+ a1: {
357
+ backgroundColor: "lightblue", // User wants to highlight corner square
358
+ },
359
+ },
360
+ showNotation: false, // User wants to hide notation
361
+ onSquareClick: jest.fn(), // User provides custom click handler
362
+ };
363
+
364
+ const result = deepMergeChessboardOptions(baseOptions, userCustomOptions);
365
+
366
+ // Move highlighting should be preserved while adding user's border
367
+ expect(result.squareStyles?.e4).toEqual({
368
+ background:
369
+ "radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)",
370
+ border: "2px solid red",
371
+ });
372
+
373
+ // User's new square style should be added
374
+ expect(result.squareStyles?.a1).toEqual({
375
+ backgroundColor: "lightblue",
376
+ });
377
+
378
+ // Other computed styles should be preserved
379
+ expect(result.squareStyles?.e2).toEqual({
380
+ backgroundColor: "rgba(255, 255, 0, 0.5)",
381
+ });
382
+ expect(result.squareStyles?.e3).toEqual({
383
+ background:
384
+ "radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)",
385
+ });
386
+
387
+ // Primitives should be overridden
388
+ expect(result.showNotation).toBe(false);
389
+ expect(result.position).toBe(
390
+ "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
391
+ );
392
+
393
+ // Functions should be replaced
394
+ expect(result.onSquareClick).toBe(userCustomOptions.onSquareClick);
395
+ expect(result.onSquareClick).not.toBe(baseOptions.onSquareClick);
396
+ });
397
+ });
143
398
  });
@@ -1,5 +1,7 @@
1
1
  import { type Chess, type Square } from "chess.js";
2
2
  import { type CSSProperties } from "react";
3
+ import { merge } from "lodash";
4
+ import type { ChessboardOptions } from "react-chessboard";
3
5
  import { getDestinationSquares, type GameInfo } from "./chess";
4
6
 
5
7
  const LAST_MOVE_COLOR = "rgba(255, 255, 0, 0.5)";
@@ -54,3 +56,50 @@ export const getCustomSquareStyles = (
54
56
  }
55
57
  return customSquareStyles;
56
58
  };
59
+
60
+ /**
61
+ * Smart deep merge for ChessboardOptions that handles different property types appropriately:
62
+ * - Functions: Overwrite (custom functions replace base functions)
63
+ * - Objects: Deep merge (nested objects merge recursively)
64
+ * - Primitives: Overwrite (custom values replace base values)
65
+ *
66
+ * This ensures that computed options (like squareStyles with move highlighting) are preserved
67
+ * while allowing custom options to extend or override them intelligently.
68
+ *
69
+ * @param baseOptions - The computed base options (e.g., computed squareStyles, event handlers)
70
+ * @param customOptions - Custom options provided by the user
71
+ * @returns Intelligently merged ChessboardOptions
72
+ */
73
+ export const deepMergeChessboardOptions = (
74
+ baseOptions: ChessboardOptions,
75
+ customOptions?: Partial<ChessboardOptions>,
76
+ ): ChessboardOptions => {
77
+ if (!customOptions) {
78
+ return { ...baseOptions }; // Return a new object even when no custom options
79
+ }
80
+
81
+ const result = merge({}, baseOptions, customOptions, {
82
+ customizer: (_objValue: unknown, srcValue: unknown) => {
83
+ // Functions should always overwrite (not merge)
84
+ // This is important for event handlers like onSquareClick, onPieceDrop, etc.
85
+ if (typeof srcValue === "function") {
86
+ return srcValue;
87
+ }
88
+
89
+ // For arrays, we typically want to overwrite rather than merge
90
+ // This avoids unexpected behavior with array concatenation
91
+ if (Array.isArray(srcValue)) {
92
+ return srcValue;
93
+ }
94
+
95
+ // Let lodash handle objects with default deep merge behavior
96
+ // This will properly merge nested objects like squareStyles, dropSquareStyle, etc.
97
+ return undefined; // Use default merge behavior
98
+ },
99
+ });
100
+
101
+ // Clean up any unwanted properties that lodash might add
102
+ delete (result as Record<string, unknown>).customizer;
103
+
104
+ return result;
105
+ };