@pie-lib/drag 3.1.1-next.0 → 3.1.1-next.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.
@@ -0,0 +1,537 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import PreviewComponent from '../preview-component';
4
+
5
+ jest.mock('@dnd-kit/core', () => ({
6
+ DragOverlay: ({ children }) => <div data-testid="drag-overlay">{children}</div>,
7
+ useDndContext: jest.fn(),
8
+ }));
9
+
10
+ jest.mock('@pie-lib/render-ui', () => ({
11
+ PreviewPrompt: ({ prompt, className, tagName }) => (
12
+ <span data-testid="preview-prompt" data-class={className} data-tag={tagName}>
13
+ {prompt}
14
+ </span>
15
+ ),
16
+ color: {
17
+ white: () => '#ffffff',
18
+ text: () => '#000000',
19
+ background: () => 'rgba(255,255,255,0)',
20
+ borderDark: () => '#646464',
21
+ },
22
+ }));
23
+
24
+ jest.mock('@pie-lib/math-rendering', () => ({
25
+ renderMath: jest.fn(),
26
+ }));
27
+
28
+ describe('PreviewComponent', () => {
29
+ const { useDndContext } = require('@dnd-kit/core');
30
+ const { renderMath } = require('@pie-lib/math-rendering');
31
+
32
+ beforeEach(() => {
33
+ jest.clearAllMocks();
34
+ window.getComputedStyle = jest.fn().mockReturnValue({ zoom: '1' });
35
+ });
36
+
37
+ afterEach(() => {
38
+ document.body.innerHTML = '';
39
+ });
40
+
41
+ describe('rendering without active drag', () => {
42
+ it('should render DragOverlay when not dragging', () => {
43
+ useDndContext.mockReturnValue({ active: null });
44
+ render(<PreviewComponent />);
45
+ expect(screen.getByTestId('drag-overlay')).toBeInTheDocument();
46
+ });
47
+
48
+ it('should not render preview content when not dragging', () => {
49
+ useDndContext.mockReturnValue({ active: null });
50
+ render(<PreviewComponent />);
51
+ expect(screen.queryByTestId('preview-prompt')).not.toBeInTheDocument();
52
+ });
53
+
54
+ it('should render empty DragOverlay when active is undefined', () => {
55
+ useDndContext.mockReturnValue({ active: undefined });
56
+ render(<PreviewComponent />);
57
+ expect(screen.getByTestId('drag-overlay')).toBeInTheDocument();
58
+ expect(screen.queryByTestId('preview-prompt')).not.toBeInTheDocument();
59
+ });
60
+ });
61
+
62
+ describe('rendering with active drag', () => {
63
+ it('should render preview when dragging with choiceId format', () => {
64
+ useDndContext.mockReturnValue({
65
+ active: {
66
+ id: 'choice-1',
67
+ data: {
68
+ current: {
69
+ choiceId: 'choice-1',
70
+ value: 'Dragging Choice',
71
+ },
72
+ },
73
+ },
74
+ });
75
+
76
+ render(<PreviewComponent />);
77
+ expect(screen.getByTestId('preview-prompt')).toBeInTheDocument();
78
+ expect(screen.getByText('Dragging Choice')).toBeInTheDocument();
79
+ });
80
+
81
+ it('should render preview when dragging with MaskBlank itemType', () => {
82
+ useDndContext.mockReturnValue({
83
+ active: {
84
+ id: 'mask-1',
85
+ data: {
86
+ current: {
87
+ itemType: 'MaskBlank',
88
+ choice: { value: 'Mask Blank Value' },
89
+ },
90
+ },
91
+ },
92
+ });
93
+
94
+ render(<PreviewComponent />);
95
+ expect(screen.getByText('Mask Blank Value')).toBeInTheDocument();
96
+ });
97
+
98
+ it('should render preview with dnd-kit-response itemType', () => {
99
+ useDndContext.mockReturnValue({
100
+ active: {
101
+ id: 'response-1',
102
+ data: {
103
+ current: {
104
+ itemType: 'dnd-kit-response',
105
+ value: 'Response Value',
106
+ },
107
+ },
108
+ },
109
+ });
110
+
111
+ render(<PreviewComponent />);
112
+ expect(screen.getByText('Response Value')).toBeInTheDocument();
113
+ });
114
+
115
+ it('should render preview with Answer itemType', () => {
116
+ useDndContext.mockReturnValue({
117
+ active: {
118
+ id: 'answer-1',
119
+ data: {
120
+ current: {
121
+ itemType: 'Answer',
122
+ value: 'Answer Value',
123
+ },
124
+ },
125
+ },
126
+ });
127
+
128
+ render(<PreviewComponent />);
129
+ expect(screen.getByText('Answer Value')).toBeInTheDocument();
130
+ });
131
+
132
+ it('should render preview with Tile itemType', () => {
133
+ useDndContext.mockReturnValue({
134
+ active: {
135
+ id: 'tile-1',
136
+ data: {
137
+ current: {
138
+ itemType: 'Tile',
139
+ value: 'Tile Value',
140
+ },
141
+ },
142
+ },
143
+ });
144
+
145
+ render(<PreviewComponent />);
146
+ expect(screen.getByText('Tile Value')).toBeInTheDocument();
147
+ });
148
+
149
+ it('should render preview with categorize itemType', () => {
150
+ useDndContext.mockReturnValue({
151
+ active: {
152
+ id: 'cat-1',
153
+ data: {
154
+ current: {
155
+ itemType: 'categorize',
156
+ value: 'Category Value',
157
+ },
158
+ },
159
+ },
160
+ });
161
+
162
+ render(<PreviewComponent />);
163
+ expect(screen.getByText('Category Value')).toBeInTheDocument();
164
+ });
165
+
166
+ it('should render preview with default itemType', () => {
167
+ useDndContext.mockReturnValue({
168
+ active: {
169
+ id: 'default-1',
170
+ data: {
171
+ current: {
172
+ itemType: 'unknown',
173
+ value: 'Default Value',
174
+ },
175
+ },
176
+ },
177
+ });
178
+
179
+ render(<PreviewComponent />);
180
+ expect(screen.getByText('Default Value')).toBeInTheDocument();
181
+ });
182
+ });
183
+
184
+ describe('PreviewPrompt integration', () => {
185
+ it('should pass correct props to PreviewPrompt', () => {
186
+ useDndContext.mockReturnValue({
187
+ active: {
188
+ id: 'test-1',
189
+ data: {
190
+ current: {
191
+ value: 'Test Prompt',
192
+ },
193
+ },
194
+ },
195
+ });
196
+
197
+ render(<PreviewComponent />);
198
+ const previewPrompt = screen.getByTestId('preview-prompt');
199
+ expect(previewPrompt).toHaveAttribute('data-class', 'prompt-label');
200
+ expect(previewPrompt).toHaveAttribute('data-tag', 'span');
201
+ });
202
+
203
+ it('should render PreviewPrompt with HTML content', () => {
204
+ useDndContext.mockReturnValue({
205
+ active: {
206
+ id: 'html-1',
207
+ data: {
208
+ current: {
209
+ value: '<strong>Bold Text</strong>',
210
+ },
211
+ },
212
+ },
213
+ });
214
+
215
+ render(<PreviewComponent />);
216
+ expect(screen.getByTestId('preview-prompt')).toBeInTheDocument();
217
+ });
218
+ });
219
+
220
+ describe('math rendering', () => {
221
+ it('should call renderMath when dragging starts', () => {
222
+ useDndContext.mockReturnValue({
223
+ active: {
224
+ id: 'math-1',
225
+ data: {
226
+ current: {
227
+ value: 'x^2 + y^2 = r^2',
228
+ },
229
+ },
230
+ },
231
+ });
232
+
233
+ render(<PreviewComponent />);
234
+ expect(renderMath).toHaveBeenCalled();
235
+ });
236
+
237
+ it('should not call renderMath when not dragging', () => {
238
+ useDndContext.mockReturnValue({ active: null });
239
+ render(<PreviewComponent />);
240
+ expect(renderMath).not.toHaveBeenCalled();
241
+ });
242
+
243
+ it('should pass correct element to renderMath', () => {
244
+ useDndContext.mockReturnValue({
245
+ active: {
246
+ id: 'math-2',
247
+ data: {
248
+ current: {
249
+ value: 'Math Expression',
250
+ },
251
+ },
252
+ },
253
+ });
254
+
255
+ render(<PreviewComponent />);
256
+ expect(renderMath).toHaveBeenCalledWith(expect.any(HTMLElement));
257
+ });
258
+ });
259
+
260
+ describe('zoom level detection', () => {
261
+ it('should detect zoom level from .padding element', () => {
262
+ // Create a mock element with zoom
263
+ const mockElement = document.createElement('div');
264
+ mockElement.className = 'padding';
265
+ document.body.appendChild(mockElement);
266
+
267
+ window.getComputedStyle = jest.fn().mockReturnValue({ zoom: '1.5' });
268
+
269
+ useDndContext.mockReturnValue({
270
+ active: {
271
+ id: 'zoom-1',
272
+ data: {
273
+ current: {
274
+ value: 'Zoomed Content',
275
+ },
276
+ },
277
+ },
278
+ });
279
+
280
+ render(<PreviewComponent />);
281
+ expect(window.getComputedStyle).toHaveBeenCalled();
282
+ });
283
+
284
+ it('should fallback to body if .padding element not found', () => {
285
+ window.getComputedStyle = jest.fn().mockReturnValue({ zoom: '1' });
286
+
287
+ useDndContext.mockReturnValue({
288
+ active: {
289
+ id: 'zoom-2',
290
+ data: {
291
+ current: {
292
+ value: 'Content',
293
+ },
294
+ },
295
+ },
296
+ });
297
+
298
+ render(<PreviewComponent />);
299
+ expect(window.getComputedStyle).toHaveBeenCalled();
300
+ });
301
+
302
+ it('should handle missing zoom value', () => {
303
+ window.getComputedStyle = jest.fn().mockReturnValue({});
304
+
305
+ useDndContext.mockReturnValue({
306
+ active: {
307
+ id: 'zoom-3',
308
+ data: {
309
+ current: {
310
+ value: 'No Zoom Content',
311
+ },
312
+ },
313
+ },
314
+ });
315
+
316
+ expect(() => render(<PreviewComponent />)).not.toThrow();
317
+ });
318
+ });
319
+
320
+ describe('style application', () => {
321
+ it('should apply base style to preview', () => {
322
+ useDndContext.mockReturnValue({
323
+ active: {
324
+ id: 'style-1',
325
+ data: {
326
+ current: {
327
+ value: 'Styled Content',
328
+ },
329
+ },
330
+ },
331
+ });
332
+
333
+ const { container } = render(<PreviewComponent />);
334
+ const styledDiv = container.querySelector('[style]');
335
+ expect(styledDiv).toBeInTheDocument();
336
+ });
337
+
338
+ it('should not render without prompt value', () => {
339
+ useDndContext.mockReturnValue({
340
+ active: {
341
+ id: 'no-prompt',
342
+ data: {
343
+ current: {
344
+ itemType: 'test',
345
+ },
346
+ },
347
+ },
348
+ });
349
+
350
+ render(<PreviewComponent />);
351
+ expect(screen.queryByTestId('preview-prompt')).not.toBeInTheDocument();
352
+ });
353
+ });
354
+
355
+ describe('edge cases', () => {
356
+ it('should handle empty string value', () => {
357
+ useDndContext.mockReturnValue({
358
+ active: {
359
+ id: 'empty',
360
+ data: {
361
+ current: {
362
+ value: '',
363
+ },
364
+ },
365
+ },
366
+ });
367
+
368
+ render(<PreviewComponent />);
369
+ expect(screen.queryByTestId('preview-prompt')).not.toBeInTheDocument();
370
+ });
371
+
372
+ it('should handle null value', () => {
373
+ useDndContext.mockReturnValue({
374
+ active: {
375
+ id: 'null',
376
+ data: {
377
+ current: {
378
+ value: null,
379
+ },
380
+ },
381
+ },
382
+ });
383
+
384
+ render(<PreviewComponent />);
385
+ expect(screen.queryByTestId('preview-prompt')).not.toBeInTheDocument();
386
+ });
387
+
388
+ it('should handle undefined value', () => {
389
+ useDndContext.mockReturnValue({
390
+ active: {
391
+ id: 'undefined',
392
+ data: {
393
+ current: {
394
+ value: undefined,
395
+ },
396
+ },
397
+ },
398
+ });
399
+
400
+ render(<PreviewComponent />);
401
+ expect(screen.queryByTestId('preview-prompt')).not.toBeInTheDocument();
402
+ });
403
+
404
+ it('should handle missing data.current', () => {
405
+ useDndContext.mockReturnValue({
406
+ active: {
407
+ id: 'no-data',
408
+ data: {},
409
+ },
410
+ });
411
+
412
+ render(<PreviewComponent />);
413
+ expect(screen.queryByTestId('preview-prompt')).not.toBeInTheDocument();
414
+ });
415
+
416
+ it('should handle MaskBlank without choice', () => {
417
+ useDndContext.mockReturnValue({
418
+ active: {
419
+ id: 'mask-no-choice',
420
+ data: {
421
+ current: {
422
+ itemType: 'MaskBlank',
423
+ },
424
+ },
425
+ },
426
+ });
427
+
428
+ render(<PreviewComponent />);
429
+ expect(screen.queryByTestId('preview-prompt')).not.toBeInTheDocument();
430
+ });
431
+
432
+ it('should handle MaskBlank with null choice', () => {
433
+ useDndContext.mockReturnValue({
434
+ active: {
435
+ id: 'mask-null-choice',
436
+ data: {
437
+ current: {
438
+ itemType: 'MaskBlank',
439
+ choice: null,
440
+ },
441
+ },
442
+ },
443
+ });
444
+
445
+ render(<PreviewComponent />);
446
+ expect(screen.queryByTestId('preview-prompt')).not.toBeInTheDocument();
447
+ });
448
+ });
449
+
450
+ describe('re-rendering', () => {
451
+ it('should update when drag state changes', () => {
452
+ useDndContext.mockReturnValue({ active: null });
453
+ const { rerender } = render(<PreviewComponent />);
454
+ expect(screen.queryByTestId('preview-prompt')).not.toBeInTheDocument();
455
+
456
+ useDndContext.mockReturnValue({
457
+ active: {
458
+ id: 'new-active',
459
+ data: {
460
+ current: {
461
+ value: 'New Active Content',
462
+ },
463
+ },
464
+ },
465
+ });
466
+
467
+ rerender(<PreviewComponent />);
468
+ expect(screen.getByText('New Active Content')).toBeInTheDocument();
469
+ });
470
+
471
+ it('should call renderMath on each update when dragging', () => {
472
+ useDndContext.mockReturnValue({
473
+ active: {
474
+ id: 'math-update',
475
+ data: {
476
+ current: {
477
+ value: 'Initial Math',
478
+ },
479
+ },
480
+ },
481
+ });
482
+
483
+ const { rerender } = render(<PreviewComponent />);
484
+ const initialCallCount = renderMath.mock.calls.length;
485
+
486
+ useDndContext.mockReturnValue({
487
+ active: {
488
+ id: 'math-update',
489
+ data: {
490
+ current: {
491
+ value: 'Updated Math',
492
+ },
493
+ },
494
+ },
495
+ });
496
+
497
+ rerender(<PreviewComponent />);
498
+ expect(renderMath.mock.calls.length).toBeGreaterThan(initialCallCount);
499
+ });
500
+ });
501
+
502
+ describe('different drag data formats', () => {
503
+ it('should handle legacy format with only value', () => {
504
+ useDndContext.mockReturnValue({
505
+ active: {
506
+ id: 'legacy',
507
+ data: {
508
+ current: {
509
+ value: 'Legacy Value',
510
+ },
511
+ },
512
+ },
513
+ });
514
+
515
+ render(<PreviewComponent />);
516
+ expect(screen.getByText('Legacy Value')).toBeInTheDocument();
517
+ });
518
+
519
+ it('should prioritize choiceId format over itemType', () => {
520
+ useDndContext.mockReturnValue({
521
+ active: {
522
+ id: 'priority',
523
+ data: {
524
+ current: {
525
+ choiceId: 'choice-1',
526
+ value: 'Choice Value',
527
+ itemType: 'categorize',
528
+ },
529
+ },
530
+ },
531
+ });
532
+
533
+ render(<PreviewComponent />);
534
+ expect(screen.getByText('Choice Value')).toBeInTheDocument();
535
+ });
536
+ });
537
+ });
@@ -0,0 +1,161 @@
1
+ import swap from '../swap';
2
+
3
+ describe('swap', () => {
4
+ describe('basic swapping', () => {
5
+ it('should swap two elements in an array', () => {
6
+ const arr = [1, 2, 3, 4, 5];
7
+ const result = swap(arr, 0, 4);
8
+ expect(result).toEqual([5, 2, 3, 4, 1]);
9
+ });
10
+
11
+ it('should swap adjacent elements', () => {
12
+ const arr = ['a', 'b', 'c'];
13
+ const result = swap(arr, 0, 1);
14
+ expect(result).toEqual(['b', 'a', 'c']);
15
+ });
16
+
17
+ it('should swap elements in the middle', () => {
18
+ const arr = [10, 20, 30, 40, 50];
19
+ const result = swap(arr, 1, 3);
20
+ expect(result).toEqual([10, 40, 30, 20, 50]);
21
+ });
22
+
23
+ it('should handle swapping the same index', () => {
24
+ const arr = [1, 2, 3];
25
+ const result = swap(arr, 1, 1);
26
+ expect(result).toEqual([1, 2, 3]);
27
+ });
28
+
29
+ it('should work with two-element array', () => {
30
+ const arr = ['first', 'second'];
31
+ const result = swap(arr, 0, 1);
32
+ expect(result).toEqual(['second', 'first']);
33
+ });
34
+ });
35
+
36
+ describe('different data types', () => {
37
+ it('should work with strings', () => {
38
+ const arr = ['apple', 'banana', 'cherry'];
39
+ const result = swap(arr, 0, 2);
40
+ expect(result).toEqual(['cherry', 'banana', 'apple']);
41
+ });
42
+
43
+ it('should work with objects', () => {
44
+ const arr = [{ name: 'John' }, { name: 'Jane' }, { name: 'Bob' }];
45
+ const result = swap(arr, 0, 2);
46
+ expect(result).toEqual([{ name: 'Bob' }, { name: 'Jane' }, { name: 'John' }]);
47
+ });
48
+
49
+ it('should work with mixed types', () => {
50
+ const arr = [1, 'two', { three: 3 }, null, undefined];
51
+ const result = swap(arr, 0, 4);
52
+ expect(result).toEqual([undefined, 'two', { three: 3 }, null, 1]);
53
+ });
54
+
55
+ it('should work with null and undefined values', () => {
56
+ const arr = [null, undefined, 'value'];
57
+ const result = swap(arr, 0, 2);
58
+ expect(result).toEqual(['value', undefined, null]);
59
+ });
60
+
61
+ it('should work with boolean values', () => {
62
+ const arr = [true, false, true];
63
+ const result = swap(arr, 0, 1);
64
+ expect(result).toEqual([false, true, true]);
65
+ });
66
+ });
67
+
68
+ describe('error handling', () => {
69
+ it('should throw error when array is null', () => {
70
+ expect(() => swap(null, 0, 1)).toThrow('swap requires a non-empty array');
71
+ });
72
+
73
+ it('should throw error when array is undefined', () => {
74
+ expect(() => swap(undefined, 0, 1)).toThrow('swap requires a non-empty array');
75
+ });
76
+
77
+ it('should throw error when array is empty', () => {
78
+ expect(() => swap([], 0, 1)).toThrow('swap requires a non-empty array');
79
+ });
80
+
81
+ it('should throw error when array has only one element', () => {
82
+ expect(() => swap([1], 0, 1)).toThrow('swap requires a non-empty array');
83
+ });
84
+
85
+ it('should throw error when fromIndex is undefined', () => {
86
+ expect(() => swap([1, 2, 3], undefined, 1)).toThrow('swap requires a non-empty array');
87
+ });
88
+
89
+ it('should throw error when toIndex is undefined', () => {
90
+ expect(() => swap([1, 2, 3], 0, undefined)).toThrow('swap requires a non-empty array');
91
+ });
92
+
93
+ it('should throw error when both indices are undefined', () => {
94
+ expect(() => swap([1, 2, 3], undefined, undefined)).toThrow('swap requires a non-empty array');
95
+ });
96
+ });
97
+
98
+ describe('edge cases', () => {
99
+ it('should handle index 0', () => {
100
+ const arr = [1, 2, 3];
101
+ const result = swap(arr, 0, 2);
102
+ expect(result).toEqual([3, 2, 1]);
103
+ });
104
+
105
+ it('should handle last index', () => {
106
+ const arr = [1, 2, 3, 4];
107
+ const result = swap(arr, 1, 3);
108
+ expect(result).toEqual([1, 4, 3, 2]);
109
+ });
110
+
111
+ it('should handle zero as a value', () => {
112
+ const arr = [0, 1, 2];
113
+ const result = swap(arr, 0, 2);
114
+ expect(result).toEqual([2, 1, 0]);
115
+ });
116
+
117
+ it('should work with large arrays', () => {
118
+ const arr = Array.from({ length: 1000 }, (_, i) => i);
119
+ const result = swap(arr, 0, 999);
120
+ expect(result[0]).toBe(999);
121
+ expect(result[999]).toBe(0);
122
+ expect(result.length).toBe(1000);
123
+ });
124
+
125
+ it('should preserve array length', () => {
126
+ const arr = [1, 2, 3, 4, 5];
127
+ const result = swap(arr, 1, 3);
128
+ expect(result.length).toBe(arr.length);
129
+ });
130
+ });
131
+
132
+ describe('complex objects', () => {
133
+ it('should handle nested arrays', () => {
134
+ const arr = [
135
+ [1, 2],
136
+ [3, 4],
137
+ [5, 6],
138
+ ];
139
+ const result = swap(arr, 0, 2);
140
+ expect(result).toEqual([
141
+ [5, 6],
142
+ [3, 4],
143
+ [1, 2],
144
+ ]);
145
+ });
146
+
147
+ it('should handle deeply nested objects', () => {
148
+ const arr = [{ a: { b: { c: 1 } } }, { a: { b: { c: 2 } } }, { a: { b: { c: 3 } } }];
149
+ const result = swap(arr, 0, 2);
150
+ expect(result).toEqual([{ a: { b: { c: 3 } } }, { a: { b: { c: 2 } } }, { a: { b: { c: 1 } } }]);
151
+ });
152
+
153
+ it('should handle objects with functions (deep clone limitation)', () => {
154
+ const fn = () => 'test';
155
+ const arr = [{ fn }, { value: 'b' }];
156
+ const result = swap(arr, 0, 1);
157
+ // Functions should be preserved by lodash cloneDeep
158
+ expect(result).toEqual([{ value: 'b' }, { fn }]);
159
+ });
160
+ });
161
+ });
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
- import { withUid, Provider as UidProvider } from '../uid-context';
3
+ import { Provider as UidProvider, withUid } from '../uid-context';
4
4
 
5
5
  describe('uid-context', () => {
6
6
  describe('withUid', () => {