@alepot55/chessboardjs 2.2.0 → 2.3.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.
Files changed (81) hide show
  1. package/.eslintrc.json +227 -0
  2. package/.github/instructions/copilot-instuctions.md +1671 -0
  3. package/README.md +127 -403
  4. package/assets/themes/alepot/theme.json +42 -0
  5. package/assets/themes/default/theme.json +42 -0
  6. package/chessboard.bundle.js +782 -119
  7. package/config/jest.config.js +15 -0
  8. package/config/rollup.config.js +35 -0
  9. package/dist/chessboard.cjs.js +10476 -0
  10. package/dist/chessboard.css +197 -0
  11. package/dist/chessboard.esm.js +10407 -0
  12. package/dist/chessboard.iife.js +10481 -0
  13. package/dist/chessboard.umd.js +10482 -0
  14. package/jest.config.js +2 -7
  15. package/package.json +18 -3
  16. package/rollup.config.js +2 -11
  17. package/{chessboard.move.js → src/components/Move.js} +3 -3
  18. package/src/components/Piece.js +273 -0
  19. package/{chessboard.square.js → src/components/Square.js} +60 -7
  20. package/src/constants/index.js +15 -0
  21. package/src/constants/positions.js +62 -0
  22. package/src/core/Chessboard.js +1930 -0
  23. package/src/core/ChessboardConfig.js +458 -0
  24. package/src/core/ChessboardFactory.js +385 -0
  25. package/src/core/index.js +141 -0
  26. package/src/errors/ChessboardError.js +133 -0
  27. package/src/errors/index.js +15 -0
  28. package/src/errors/messages.js +189 -0
  29. package/src/index.js +103 -0
  30. package/src/services/AnimationService.js +180 -0
  31. package/src/services/BoardService.js +156 -0
  32. package/src/services/CoordinateService.js +355 -0
  33. package/src/services/EventService.js +807 -0
  34. package/src/services/MoveService.js +594 -0
  35. package/src/services/PieceService.js +303 -0
  36. package/src/services/PositionService.js +237 -0
  37. package/src/services/ValidationService.js +673 -0
  38. package/src/services/index.js +14 -0
  39. package/src/styles/animations.css +46 -0
  40. package/{chessboard.css → src/styles/board.css} +3 -0
  41. package/src/styles/index.css +4 -0
  42. package/src/styles/pieces.css +66 -0
  43. package/src/utils/animations.js +37 -0
  44. package/{chess.js → src/utils/chess.js} +16 -16
  45. package/src/utils/coordinates.js +62 -0
  46. package/src/utils/cross-browser.js +150 -0
  47. package/src/utils/logger.js +422 -0
  48. package/src/utils/performance.js +311 -0
  49. package/src/utils/validation.js +458 -0
  50. package/tests/unit/chessboard-config-animations.test.js +106 -0
  51. package/tests/unit/chessboard-robust.test.js +163 -0
  52. package/tests/unit/chessboard.test.js +183 -0
  53. package/chessboard.config.js +0 -147
  54. package/chessboard.js +0 -981
  55. package/chessboard.piece.js +0 -115
  56. package/test/chessboard.test.js +0 -128
  57. /package/{alepot_theme → assets/themes/alepot}/bb.svg +0 -0
  58. /package/{alepot_theme → assets/themes/alepot}/bw.svg +0 -0
  59. /package/{alepot_theme → assets/themes/alepot}/kb.svg +0 -0
  60. /package/{alepot_theme → assets/themes/alepot}/kw.svg +0 -0
  61. /package/{alepot_theme → assets/themes/alepot}/nb.svg +0 -0
  62. /package/{alepot_theme → assets/themes/alepot}/nw.svg +0 -0
  63. /package/{alepot_theme → assets/themes/alepot}/pb.svg +0 -0
  64. /package/{alepot_theme → assets/themes/alepot}/pw.svg +0 -0
  65. /package/{alepot_theme → assets/themes/alepot}/qb.svg +0 -0
  66. /package/{alepot_theme → assets/themes/alepot}/qw.svg +0 -0
  67. /package/{alepot_theme → assets/themes/alepot}/rb.svg +0 -0
  68. /package/{alepot_theme → assets/themes/alepot}/rw.svg +0 -0
  69. /package/{default_pieces → assets/themes/default}/bb.svg +0 -0
  70. /package/{default_pieces → assets/themes/default}/bw.svg +0 -0
  71. /package/{default_pieces → assets/themes/default}/kb.svg +0 -0
  72. /package/{default_pieces → assets/themes/default}/kw.svg +0 -0
  73. /package/{default_pieces → assets/themes/default}/nb.svg +0 -0
  74. /package/{default_pieces → assets/themes/default}/nw.svg +0 -0
  75. /package/{default_pieces → assets/themes/default}/pb.svg +0 -0
  76. /package/{default_pieces → assets/themes/default}/pw.svg +0 -0
  77. /package/{default_pieces → assets/themes/default}/qb.svg +0 -0
  78. /package/{default_pieces → assets/themes/default}/qw.svg +0 -0
  79. /package/{default_pieces → assets/themes/default}/rb.svg +0 -0
  80. /package/{default_pieces → assets/themes/default}/rw.svg +0 -0
  81. /package/{.babelrc → config/.babelrc} +0 -0
@@ -0,0 +1,458 @@
1
+ /**
2
+ * Validation utilities for Chessboard.js
3
+ * @module utils/validation
4
+ * @since 2.0.0
5
+ */
6
+
7
+ // Re-export from coordinates utility
8
+ export { isValidSquare } from './coordinates.js';
9
+
10
+ /**
11
+ * Validation cache for performance optimization
12
+ * @type {Map}
13
+ */
14
+ const validationCache = new Map();
15
+ const CACHE_MAX_SIZE = 1000;
16
+
17
+ /**
18
+ * Caches validation result
19
+ * @param {string} key - Cache key
20
+ * @param {boolean} result - Validation result
21
+ */
22
+ function cacheResult(key, result) {
23
+ if (validationCache.size >= CACHE_MAX_SIZE) {
24
+ const firstKey = validationCache.keys().next().value;
25
+ validationCache.delete(firstKey);
26
+ }
27
+ validationCache.set(key, result);
28
+ }
29
+
30
+ /**
31
+ * Validate piece notation with caching
32
+ * @param {string} piece - Piece notation (e.g., 'wP', 'bK')
33
+ * @returns {boolean} True if piece notation is valid
34
+ */
35
+ export function isValidPiece(piece) {
36
+ if (typeof piece !== 'string' || piece.length !== 2) {
37
+ return false;
38
+ }
39
+
40
+ const cacheKey = `piece:${piece}`;
41
+ if (validationCache.has(cacheKey)) {
42
+ return validationCache.get(cacheKey);
43
+ }
44
+
45
+ const color = piece[0];
46
+ const type = piece[1];
47
+
48
+ const isValid = ['w', 'b'].includes(color) &&
49
+ ['P', 'R', 'N', 'B', 'Q', 'K'].includes(type);
50
+
51
+ cacheResult(cacheKey, isValid);
52
+ return isValid;
53
+ }
54
+
55
+ /**
56
+ * Validate position object with comprehensive checks
57
+ * @param {Object} position - Position object with square-piece mappings
58
+ * @returns {boolean} True if position is valid
59
+ */
60
+ export function isValidPosition(position) {
61
+ if (typeof position !== 'object' || position === null) {
62
+ return false;
63
+ }
64
+
65
+ // Check for circular references
66
+ try {
67
+ JSON.stringify(position);
68
+ } catch (error) {
69
+ return false;
70
+ }
71
+
72
+ // Validate each square-piece mapping
73
+ for (const [square, piece] of Object.entries(position)) {
74
+ if (!isValidSquare(square) || !isValidPiece(piece)) {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ // Optional: Check for chess-specific rules
80
+ return isValidChessPosition(position);
81
+ }
82
+
83
+ /**
84
+ * Validates chess-specific position rules
85
+ * @param {Object} position - Position object
86
+ * @returns {boolean} True if position follows chess rules
87
+ */
88
+ function isValidChessPosition(position) {
89
+ const pieces = Object.values(position);
90
+
91
+ // Count kings
92
+ const whiteKings = pieces.filter(p => p === 'wK').length;
93
+ const blackKings = pieces.filter(p => p === 'bK').length;
94
+
95
+ // Must have exactly one king of each color
96
+ if (whiteKings !== 1 || blackKings !== 1) {
97
+ return false;
98
+ }
99
+
100
+ // Count pawns (max 8 per side)
101
+ const whitePawns = pieces.filter(p => p === 'wP').length;
102
+ const blackPawns = pieces.filter(p => p === 'bP').length;
103
+
104
+ if (whitePawns > 8 || blackPawns > 8) {
105
+ return false;
106
+ }
107
+
108
+ // Check pawn placement (not on first or last rank)
109
+ for (const [square, piece] of Object.entries(position)) {
110
+ if (piece === 'wP' || piece === 'bP') {
111
+ const rank = square[1];
112
+ if (rank === '1' || rank === '8') {
113
+ return false;
114
+ }
115
+ }
116
+ }
117
+
118
+ return true;
119
+ }
120
+
121
+ /**
122
+ * Validate FEN string format with comprehensive checks
123
+ * @param {string} fen - FEN string
124
+ * @returns {Object} Validation result with success and error properties
125
+ */
126
+ export function validateFenFormat(fen) {
127
+ if (typeof fen !== 'string') {
128
+ return { success: false, error: 'FEN must be a string' };
129
+ }
130
+
131
+ const cacheKey = `fen:${fen}`;
132
+ if (validationCache.has(cacheKey)) {
133
+ return validationCache.get(cacheKey);
134
+ }
135
+
136
+ const parts = fen.trim().split(' ');
137
+ if (parts.length !== 6) {
138
+ const result = { success: false, error: 'FEN must have 6 parts separated by spaces' };
139
+ cacheResult(cacheKey, result);
140
+ return result;
141
+ }
142
+
143
+ // Validate piece placement
144
+ const ranks = parts[0].split('/');
145
+ if (ranks.length !== 8) {
146
+ const result = { success: false, error: 'Piece placement must have 8 ranks' };
147
+ cacheResult(cacheKey, result);
148
+ return result;
149
+ }
150
+
151
+ // Validate each rank
152
+ for (let i = 0; i < ranks.length; i++) {
153
+ const rankResult = validateRank(ranks[i]);
154
+ if (!rankResult.success) {
155
+ const result = { success: false, error: `Invalid rank ${i + 1}: ${rankResult.error}` };
156
+ cacheResult(cacheKey, result);
157
+ return result;
158
+ }
159
+ }
160
+
161
+ // Validate active color
162
+ if (!['w', 'b'].includes(parts[1])) {
163
+ const result = { success: false, error: 'Active color must be "w" or "b"' };
164
+ cacheResult(cacheKey, result);
165
+ return result;
166
+ }
167
+
168
+ // Validate castling availability
169
+ if (!/^[KQkq-]*$/.test(parts[2])) {
170
+ const result = { success: false, error: 'Invalid castling availability' };
171
+ cacheResult(cacheKey, result);
172
+ return result;
173
+ }
174
+
175
+ // Validate en passant target square
176
+ if (parts[3] !== '-' && !isValidSquare(parts[3])) {
177
+ const result = { success: false, error: 'Invalid en passant target square' };
178
+ cacheResult(cacheKey, result);
179
+ return result;
180
+ }
181
+
182
+ // Validate halfmove clock
183
+ const halfmove = parseInt(parts[4], 10);
184
+ if (isNaN(halfmove) || halfmove < 0) {
185
+ const result = { success: false, error: 'Invalid halfmove clock' };
186
+ cacheResult(cacheKey, result);
187
+ return result;
188
+ }
189
+
190
+ // Validate fullmove number
191
+ const fullmove = parseInt(parts[5], 10);
192
+ if (isNaN(fullmove) || fullmove < 1) {
193
+ const result = { success: false, error: 'Invalid fullmove number' };
194
+ cacheResult(cacheKey, result);
195
+ return result;
196
+ }
197
+
198
+ const result = { success: true };
199
+ cacheResult(cacheKey, result);
200
+ return result;
201
+ }
202
+
203
+ /**
204
+ * Validates a single rank in FEN notation
205
+ * @param {string} rank - Rank string
206
+ * @returns {Object} Validation result
207
+ */
208
+ function validateRank(rank) {
209
+ if (typeof rank !== 'string') {
210
+ return { success: false, error: 'Rank must be a string' };
211
+ }
212
+
213
+ let squareCount = 0;
214
+
215
+ for (let i = 0; i < rank.length; i++) {
216
+ const char = rank[i];
217
+
218
+ if (/[1-8]/.test(char)) {
219
+ squareCount += parseInt(char, 10);
220
+ } else if (/[prnbqkPRNBQK]/.test(char)) {
221
+ squareCount += 1;
222
+ } else {
223
+ return { success: false, error: `Invalid character "${char}" in rank` };
224
+ }
225
+ }
226
+
227
+ if (squareCount !== 8) {
228
+ return { success: false, error: `Rank must represent exactly 8 squares, got ${squareCount}` };
229
+ }
230
+
231
+ return { success: true };
232
+ }
233
+
234
+ /**
235
+ * Validate move notation in various formats
236
+ * @param {string} move - Move string (e.g., 'e2e4', 'Nf3', 'O-O')
237
+ * @returns {Object} Validation result
238
+ */
239
+ export function validateMove(move) {
240
+ if (typeof move !== 'string') {
241
+ return { success: false, error: 'Move must be a string' };
242
+ }
243
+
244
+ const trimmed = move.trim();
245
+ if (trimmed.length === 0) {
246
+ return { success: false, error: 'Move cannot be empty' };
247
+ }
248
+
249
+ // Check for castling
250
+ if (trimmed === 'O-O' || trimmed === 'O-O-O') {
251
+ return { success: true, type: 'castling' };
252
+ }
253
+
254
+ // Check for coordinate notation (e.g., e2e4, e7e8q)
255
+ if (/^[a-h][1-8][a-h][1-8][qrnbQRNB]?$/.test(trimmed)) {
256
+ return { success: true, type: 'coordinate' };
257
+ }
258
+
259
+ // Check for algebraic notation (e.g., Nf3, Qxd5, e4)
260
+ if (/^[PRNBQK]?[a-h]?[1-8]?x?[a-h][1-8](\+|#)?$/.test(trimmed)) {
261
+ return { success: true, type: 'algebraic' };
262
+ }
263
+
264
+ return { success: false, error: 'Invalid move format' };
265
+ }
266
+
267
+ /**
268
+ * Validate configuration object with detailed error reporting
269
+ * @param {Object} config - Configuration object
270
+ * @returns {Object} Validation result with success and errors array
271
+ */
272
+ export function validateConfig(config) {
273
+ const errors = [];
274
+
275
+ if (!config || typeof config !== 'object') {
276
+ return { success: false, errors: ['Configuration must be an object'] };
277
+ }
278
+
279
+ // Required fields
280
+ if (!config.id && !config.id_div) {
281
+ errors.push('Configuration must include "id" or "id_div"');
282
+ }
283
+
284
+ // Optional field validation
285
+ if (config.orientation && !['white', 'black', 'w', 'b'].includes(config.orientation)) {
286
+ errors.push('Invalid orientation. Must be "white", "black", "w", or "b"');
287
+ }
288
+
289
+ if (config.position && config.position !== 'start' && typeof config.position !== 'object') {
290
+ errors.push('Invalid position. Must be "start" or a position object');
291
+ }
292
+
293
+ if (config.position && typeof config.position === 'object' && !isValidPosition(config.position)) {
294
+ errors.push('Invalid position object format');
295
+ }
296
+
297
+ if (config.size && !isValidSize(config.size)) {
298
+ errors.push('Invalid size. Must be "auto", a positive number, or a valid CSS size');
299
+ }
300
+
301
+ if (config.movableColors && !['white', 'black', 'w', 'b', 'both', 'none'].includes(config.movableColors)) {
302
+ errors.push('Invalid movableColors. Must be "white", "black", "w", "b", "both", or "none"');
303
+ }
304
+
305
+ if (config.dropOffBoard && !['snapback', 'trash'].includes(config.dropOffBoard)) {
306
+ errors.push('Invalid dropOffBoard. Must be "snapback" or "trash"');
307
+ }
308
+
309
+ // Validate callback functions
310
+ const callbacks = ['onMove', 'onMoveEnd', 'onChange', 'onDragStart', 'onDragMove', 'onDrop', 'onSnapbackEnd'];
311
+ for (const callback of callbacks) {
312
+ if (config[callback] && typeof config[callback] !== 'function') {
313
+ errors.push(`Invalid ${callback}. Must be a function`);
314
+ }
315
+ }
316
+
317
+ // Validate color values
318
+ const colorFields = ['whiteSquare', 'blackSquare', 'highlight', 'hintColor'];
319
+ for (const field of colorFields) {
320
+ if (config[field] && !isValidColor(config[field])) {
321
+ errors.push(`Invalid ${field}. Must be a valid CSS color`);
322
+ }
323
+ }
324
+
325
+ // Validate animation settings
326
+ if (config.moveAnimation && !isValidEasing(config.moveAnimation)) {
327
+ errors.push('Invalid moveAnimation. Must be a valid easing function');
328
+ }
329
+
330
+ if (config.animationStyle && !['sequential', 'simultaneous'].includes(config.animationStyle)) {
331
+ errors.push('Invalid animationStyle. Must be "sequential" or "simultaneous"');
332
+ }
333
+
334
+ return {
335
+ success: errors.length === 0,
336
+ errors
337
+ };
338
+ }
339
+
340
+ /**
341
+ * Validates size value
342
+ * @param {number|string} size - Size value
343
+ * @returns {boolean} True if valid
344
+ */
345
+ function isValidSize(size) {
346
+ if (size === 'auto') {
347
+ return true;
348
+ }
349
+
350
+ if (typeof size === 'number') {
351
+ return size > 0 && size <= 5000; // Reasonable upper limit
352
+ }
353
+
354
+ if (typeof size === 'string') {
355
+ // Check for CSS size values
356
+ return /^\d+(px|em|rem|%|vh|vw)$/.test(size);
357
+ }
358
+
359
+ return false;
360
+ }
361
+
362
+ /**
363
+ * Validates CSS color value
364
+ * @param {string} color - Color value
365
+ * @returns {boolean} True if valid
366
+ */
367
+ function isValidColor(color) {
368
+ if (typeof color !== 'string') {
369
+ return false;
370
+ }
371
+
372
+ // Hex colors
373
+ if (/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(color)) {
374
+ return true;
375
+ }
376
+
377
+ // RGB/RGBA
378
+ if (/^rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(,\s*[0-1](\.\d+)?)?\s*\)$/.test(color)) {
379
+ return true;
380
+ }
381
+
382
+ // HSL/HSLA
383
+ if (/^hsla?\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*(,\s*[0-1](\.\d+)?)?\s*\)$/.test(color)) {
384
+ return true;
385
+ }
386
+
387
+ // Named colors (basic set)
388
+ const namedColors = [
389
+ 'white', 'black', 'red', 'green', 'blue', 'yellow', 'cyan', 'magenta',
390
+ 'transparent', 'gray', 'grey', 'brown', 'orange', 'purple', 'pink'
391
+ ];
392
+
393
+ return namedColors.includes(color.toLowerCase());
394
+ }
395
+
396
+ /**
397
+ * Validates easing function
398
+ * @param {string} easing - Easing function name
399
+ * @returns {boolean} True if valid
400
+ */
401
+ function isValidEasing(easing) {
402
+ const validEasing = [
403
+ 'linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out',
404
+ 'cubic-bezier'
405
+ ];
406
+
407
+ return validEasing.includes(easing) ||
408
+ /^cubic-bezier\(\s*[\d.-]+\s*,\s*[\d.-]+\s*,\s*[\d.-]+\s*,\s*[\d.-]+\s*\)$/.test(easing);
409
+ }
410
+
411
+ /**
412
+ * Batch validates multiple items
413
+ * @param {Array} items - Array of validation items
414
+ * @returns {Array} Array of validation results
415
+ */
416
+ export function batchValidate(items) {
417
+ return items.map(item => {
418
+ const { type, value } = item;
419
+
420
+ switch (type) {
421
+ case 'piece':
422
+ return { ...item, valid: isValidPiece(value) };
423
+ case 'square':
424
+ return { ...item, valid: isValidSquare(value) };
425
+ case 'position':
426
+ return { ...item, valid: isValidPosition(value) };
427
+ case 'fen':
428
+ const fenResult = validateFenFormat(value);
429
+ return { ...item, valid: fenResult.success, error: fenResult.error };
430
+ case 'move':
431
+ const moveResult = validateMove(value);
432
+ return { ...item, valid: moveResult.success, error: moveResult.error };
433
+ case 'config':
434
+ const configResult = validateConfig(value);
435
+ return { ...item, valid: configResult.success, errors: configResult.errors };
436
+ default:
437
+ return { ...item, valid: false, error: 'Unknown validation type' };
438
+ }
439
+ });
440
+ }
441
+
442
+ /**
443
+ * Clears validation cache
444
+ */
445
+ export function clearValidationCache() {
446
+ validationCache.clear();
447
+ }
448
+
449
+ /**
450
+ * Gets validation cache statistics
451
+ * @returns {Object} Cache statistics
452
+ */
453
+ export function getValidationCacheStats() {
454
+ return {
455
+ size: validationCache.size,
456
+ maxSize: CACHE_MAX_SIZE
457
+ };
458
+ }
@@ -0,0 +1,106 @@
1
+ import { ChessboardConfig } from '../../src/core/ChessboardConfig.js';
2
+
3
+ describe('ChessboardConfig Animation & Drag Properties', () => {
4
+ const baseConfig = { id: 'test-board' };
5
+
6
+ test('should handle boolean moveAnimation values', () => {
7
+ expect(() => {
8
+ new ChessboardConfig({ ...baseConfig, moveAnimation: true });
9
+ }).not.toThrow();
10
+ expect(() => {
11
+ new ChessboardConfig({ ...baseConfig, moveAnimation: false });
12
+ }).not.toThrow();
13
+ // Check resulting property
14
+ expect(new ChessboardConfig({ ...baseConfig, moveAnimation: true }).moveAnimation).toBe('ease');
15
+ expect(new ChessboardConfig({ ...baseConfig, moveAnimation: false }).moveAnimation).toBe(null);
16
+ });
17
+
18
+ test('should handle boolean snapbackAnimation values', () => {
19
+ expect(() => {
20
+ new ChessboardConfig({ ...baseConfig, snapbackAnimation: true });
21
+ }).not.toThrow();
22
+ expect(() => {
23
+ new ChessboardConfig({ ...baseConfig, snapbackAnimation: false });
24
+ }).not.toThrow();
25
+ expect(new ChessboardConfig({ ...baseConfig, snapbackAnimation: true }).snapbackAnimation).toBe('ease');
26
+ expect(new ChessboardConfig({ ...baseConfig, snapbackAnimation: false }).snapbackAnimation).toBe(null);
27
+ });
28
+
29
+ test('should handle boolean fadeAnimation values', () => {
30
+ expect(() => {
31
+ new ChessboardConfig({ ...baseConfig, fadeAnimation: true });
32
+ }).not.toThrow();
33
+ expect(() => {
34
+ new ChessboardConfig({ ...baseConfig, fadeAnimation: false });
35
+ }).not.toThrow();
36
+ expect(new ChessboardConfig({ ...baseConfig, fadeAnimation: true }).fadeAnimation).toBe('ease');
37
+ expect(new ChessboardConfig({ ...baseConfig, fadeAnimation: false }).fadeAnimation).toBe(null);
38
+ });
39
+
40
+ test('should handle string animation values', () => {
41
+ expect(() => {
42
+ new ChessboardConfig({
43
+ ...baseConfig,
44
+ moveAnimation: 'ease-in-out',
45
+ snapbackAnimation: 'linear',
46
+ fadeAnimation: 'ease'
47
+ });
48
+ }).not.toThrow();
49
+ });
50
+
51
+ test('should handle none animation values', () => {
52
+ expect(() => {
53
+ new ChessboardConfig({
54
+ ...baseConfig,
55
+ moveAnimation: 'none',
56
+ snapbackAnimation: 'none',
57
+ fadeAnimation: 'none'
58
+ });
59
+ }).not.toThrow();
60
+ });
61
+
62
+ test('should throw error for invalid animation values', () => {
63
+ expect(() => {
64
+ new ChessboardConfig({ ...baseConfig, moveAnimation: 'invalid-animation' });
65
+ }).toThrow('Invalid transition function');
66
+ });
67
+
68
+ // --- DRAGGABLE ---
69
+ test('should handle draggable true/false', () => {
70
+ expect(() => new ChessboardConfig({ ...baseConfig, draggable: true })).not.toThrow();
71
+ expect(() => new ChessboardConfig({ ...baseConfig, draggable: false })).not.toThrow();
72
+ });
73
+ test('should default draggable to true', () => {
74
+ const config = new ChessboardConfig(baseConfig);
75
+ expect(config.draggable).toBe(true);
76
+ });
77
+ test('should throw error for invalid draggable value', () => {
78
+ expect(() => new ChessboardConfig({ ...baseConfig, draggable: 'maybe' })).toThrow();
79
+ });
80
+
81
+ // --- DROP OFF BOARD ---
82
+ test('should accept valid dropOffBoard values', () => {
83
+ expect(() => new ChessboardConfig({ ...baseConfig, dropOffBoard: 'snapback' })).not.toThrow();
84
+ expect(() => new ChessboardConfig({ ...baseConfig, dropOffBoard: 'trash' })).not.toThrow();
85
+ });
86
+ test('should throw error for invalid dropOffBoard value', () => {
87
+ expect(() => new ChessboardConfig({ ...baseConfig, dropOffBoard: 'invalid' })).toThrow();
88
+ });
89
+
90
+ // --- ANIMATION STYLE ---
91
+ test('should accept valid animationStyle values', () => {
92
+ expect(() => new ChessboardConfig({ ...baseConfig, animationStyle: 'simultaneous' })).not.toThrow();
93
+ expect(() => new ChessboardConfig({ ...baseConfig, animationStyle: 'sequential' })).not.toThrow();
94
+ });
95
+ test('should throw error for invalid animationStyle value', () => {
96
+ expect(() => new ChessboardConfig({ ...baseConfig, animationStyle: 'crazy' })).toThrow();
97
+ });
98
+
99
+ // --- EDGE CASES ---
100
+ test('should throw error if id/id_div is missing', () => {
101
+ expect(() => new ChessboardConfig({})).toThrow();
102
+ });
103
+ test('should throw error for non-object config', () => {
104
+ expect(() => new ChessboardConfig('not-an-object')).toThrow();
105
+ });
106
+ });