queueConflictResolution.test.ts690 lines · main
1// @ts-nocheck
2import { describe, expect, test } from 'vitest'
3
4import {
5 operationMatchesRow,
6 resolveDeleteRowConflicts,
7 resolveEditCellConflicts,
8 upsertOperation,
9} from './queueConflictResolution'
10import {
11 QueuedOperation,
12 QueuedOperationType,
13 type NewAddRowOperation,
14 type NewDeleteRowOperation,
15 type NewEditCellContentOperation,
16} from '@/state/table-editor-operation-queue.types'
17
18describe('operationMatchesRow', () => {
19 const mockTable = {} as any
20
21 test('should match EDIT_CELL_CONTENT operation with same row identifiers', () => {
22 const operation: QueuedOperation = {
23 id: 'edit_cell_content:1:name:id:1',
24 type: QueuedOperationType.EDIT_CELL_CONTENT,
25 tableId: 1,
26 timestamp: Date.now(),
27 payload: {
28 rowIdentifiers: { id: 1 },
29 columnName: 'name',
30 oldValue: 'old',
31 newValue: 'new',
32 table: mockTable,
33 },
34 }
35 expect(operationMatchesRow(operation, 1, { id: 1 })).toBe(true)
36 })
37
38 test('should not match EDIT_CELL_CONTENT operation with different row identifiers', () => {
39 const operation: QueuedOperation = {
40 id: 'edit_cell_content:1:name:id:1',
41 type: QueuedOperationType.EDIT_CELL_CONTENT,
42 tableId: 1,
43 timestamp: Date.now(),
44 payload: {
45 rowIdentifiers: { id: 1 },
46 columnName: 'name',
47 oldValue: 'old',
48 newValue: 'new',
49 table: mockTable,
50 },
51 }
52 expect(operationMatchesRow(operation, 1, { id: 2 })).toBe(false)
53 })
54
55 test('should not match operation from different table', () => {
56 const operation: QueuedOperation = {
57 id: 'edit_cell_content:1:name:id:1',
58 type: QueuedOperationType.EDIT_CELL_CONTENT,
59 tableId: 1,
60 timestamp: Date.now(),
61 payload: {
62 rowIdentifiers: { id: 1 },
63 columnName: 'name',
64 oldValue: 'old',
65 newValue: 'new',
66 table: mockTable,
67 },
68 }
69 expect(operationMatchesRow(operation, 2, { id: 1 })).toBe(false)
70 })
71
72 test('should match DELETE_ROW operation with same row identifiers', () => {
73 const operation: QueuedOperation = {
74 id: 'delete_row:1:id:1',
75 type: QueuedOperationType.DELETE_ROW,
76 tableId: 1,
77 timestamp: Date.now(),
78 payload: {
79 rowIdentifiers: { id: 1 },
80 originalRow: { idx: 1, id: 1, name: 'test' },
81 table: mockTable,
82 },
83 }
84 expect(operationMatchesRow(operation, 1, { id: 1 })).toBe(true)
85 })
86
87 test('should return false for ADD_ROW operations', () => {
88 const operation: QueuedOperation = {
89 id: 'add_row:1:temp123',
90 type: QueuedOperationType.ADD_ROW,
91 tableId: 1,
92 timestamp: Date.now(),
93 payload: {
94 tempId: 'temp123',
95 rowData: { idx: 1, __tempId: '1', name: 'new' },
96 table: mockTable,
97 },
98 }
99 expect(operationMatchesRow(operation, 1, { id: 1 })).toBe(false)
100 })
101})
102
103describe('resolveDeleteRowConflicts', () => {
104 const mockTable = {} as any
105
106 test('should skip delete and remove ADD_ROW when deleting a newly added row', () => {
107 const addRowOp: QueuedOperation = {
108 id: 'add_row:1:-12345',
109 type: QueuedOperationType.ADD_ROW,
110 tableId: 1,
111 timestamp: Date.now(),
112 payload: {
113 tempId: '-12345',
114 rowData: { idx: -12345, __tempId: '-12345', name: 'new row' },
115 table: mockTable,
116 },
117 }
118
119 const operations = [addRowOp]
120 const deleteOperation: NewDeleteRowOperation = {
121 type: QueuedOperationType.DELETE_ROW,
122 tableId: 1,
123 payload: {
124 rowIdentifiers: { __tempId: '-12345' },
125 originalRow: { idx: -12345, __tempId: '-12345', name: 'new row' },
126 table: mockTable,
127 },
128 }
129
130 const result = resolveDeleteRowConflicts(operations, deleteOperation)
131
132 expect(result.action).toBe('skip')
133 expect(result.filteredOperations).toEqual([])
134 })
135
136 test('should skip delete and remove ADD_ROW and related EDIT_CELLs when deleting a newly added row', () => {
137 const addRowOp: QueuedOperation = {
138 id: 'add_row:1:-12345',
139 type: QueuedOperationType.ADD_ROW,
140 tableId: 1,
141 timestamp: Date.now(),
142 payload: {
143 tempId: '-12345',
144 rowData: { idx: -12345, __tempId: '-12345', name: 'new row' },
145 table: mockTable,
146 },
147 }
148
149 const editCellOp: QueuedOperation = {
150 id: 'edit_cell_content:1:name:__tempId:-12345',
151 type: QueuedOperationType.EDIT_CELL_CONTENT,
152 tableId: 1,
153 timestamp: Date.now(),
154 payload: {
155 rowIdentifiers: { __tempId: '-12345' },
156 columnName: 'name',
157 oldValue: 'new row',
158 newValue: 'edited',
159 table: mockTable,
160 },
161 }
162
163 const otherEditOp: QueuedOperation = {
164 id: 'edit_cell_content:1:name:id:99',
165 type: QueuedOperationType.EDIT_CELL_CONTENT,
166 tableId: 1,
167 timestamp: Date.now(),
168 payload: {
169 rowIdentifiers: { id: 99 },
170 columnName: 'name',
171 oldValue: 'original',
172 newValue: 'changed',
173 table: mockTable,
174 },
175 }
176
177 const operations = [addRowOp, editCellOp, otherEditOp]
178 const deleteOperation: NewDeleteRowOperation = {
179 type: QueuedOperationType.DELETE_ROW,
180 tableId: 1,
181 payload: {
182 rowIdentifiers: { __tempId: '-12345' },
183 originalRow: { idx: -12345, __tempId: '-12345', name: 'new row' },
184 table: mockTable,
185 },
186 }
187
188 const result = resolveDeleteRowConflicts(operations, deleteOperation)
189
190 expect(result.action).toBe('skip')
191 expect(result.filteredOperations).toHaveLength(1)
192 expect(result.filteredOperations[0]).toEqual(otherEditOp)
193 })
194
195 test('should add delete and remove EDIT_CELLs for existing row being deleted', () => {
196 const editCellOp: QueuedOperation = {
197 id: 'edit_cell_content:1:name:id:1',
198 type: QueuedOperationType.EDIT_CELL_CONTENT,
199 tableId: 1,
200 timestamp: Date.now(),
201 payload: {
202 rowIdentifiers: { id: 1 },
203 columnName: 'name',
204 oldValue: 'original',
205 newValue: 'edited',
206 table: mockTable,
207 },
208 }
209
210 const otherEditOp: QueuedOperation = {
211 id: 'edit_cell_content:1:name:id:2',
212 type: QueuedOperationType.EDIT_CELL_CONTENT,
213 tableId: 1,
214 timestamp: Date.now(),
215 payload: {
216 rowIdentifiers: { id: 2 },
217 columnName: 'name',
218 oldValue: 'other',
219 newValue: 'changed',
220 table: mockTable,
221 },
222 }
223
224 const operations = [editCellOp, otherEditOp]
225 const deleteOperation: NewDeleteRowOperation = {
226 type: QueuedOperationType.DELETE_ROW,
227 tableId: 1,
228 payload: {
229 rowIdentifiers: { id: 1 },
230 originalRow: { idx: 1, id: 1, name: 'original' },
231 table: mockTable,
232 },
233 }
234
235 const result = resolveDeleteRowConflicts(operations, deleteOperation)
236
237 expect(result.action).toBe('add')
238 expect(result.filteredOperations).toHaveLength(1)
239 expect(result.filteredOperations[0]).toEqual(otherEditOp)
240 })
241
242 test('should add delete with no changes when there are no conflicts', () => {
243 const otherEditOp: QueuedOperation = {
244 id: 'edit_cell_content:1:name:id:2',
245 type: QueuedOperationType.EDIT_CELL_CONTENT,
246 tableId: 1,
247 timestamp: Date.now(),
248 payload: {
249 rowIdentifiers: { id: 2 },
250 columnName: 'name',
251 oldValue: 'other',
252 newValue: 'changed',
253 table: mockTable,
254 },
255 }
256
257 const operations = [otherEditOp]
258 const deleteOperation: NewDeleteRowOperation = {
259 type: QueuedOperationType.DELETE_ROW,
260 tableId: 1,
261 payload: {
262 rowIdentifiers: { id: 1 },
263 originalRow: { idx: 1, id: 1, name: 'original' },
264 table: mockTable,
265 },
266 }
267
268 const result = resolveDeleteRowConflicts(operations, deleteOperation)
269
270 expect(result.action).toBe('add')
271 expect(result.filteredOperations).toEqual(operations)
272 })
273})
274
275describe('resolveEditCellConflicts', () => {
276 const mockTable = {} as any
277
278 test('should reject edit on a row pending deletion', () => {
279 const deleteOp: QueuedOperation = {
280 id: 'delete_row:1:id:1',
281 type: QueuedOperationType.DELETE_ROW,
282 tableId: 1,
283 timestamp: Date.now(),
284 payload: {
285 rowIdentifiers: { id: 1 },
286 originalRow: { idx: 1, id: 1, name: 'to delete' },
287 table: mockTable,
288 },
289 }
290
291 const operations = [deleteOp]
292 const editOperation: NewEditCellContentOperation = {
293 type: QueuedOperationType.EDIT_CELL_CONTENT,
294 tableId: 1,
295 payload: {
296 rowIdentifiers: { id: 1 },
297 columnName: 'name',
298 oldValue: 'to delete',
299 newValue: 'changed',
300 table: mockTable,
301 },
302 }
303
304 const result = resolveEditCellConflicts(operations, editOperation)
305
306 expect(result.action).toBe('reject')
307 if (result.action === 'reject') {
308 expect(result.reason).toContain('pending deletion')
309 }
310 })
311
312 test('should merge edit into ADD_ROW for a newly added row', () => {
313 const addRowOp: QueuedOperation = {
314 id: 'add_row:1:-12345',
315 type: QueuedOperationType.ADD_ROW,
316 tableId: 1,
317 timestamp: Date.now(),
318 payload: {
319 tempId: '-12345',
320 rowData: { idx: -12345, __tempId: '-12345', name: 'new row' },
321 table: mockTable,
322 },
323 }
324
325 const operations = [addRowOp]
326 const editOperation: NewEditCellContentOperation = {
327 type: QueuedOperationType.EDIT_CELL_CONTENT,
328 tableId: 1,
329 payload: {
330 rowIdentifiers: { __tempId: '-12345' },
331 columnName: 'name',
332 oldValue: 'new row',
333 newValue: 'edited value',
334 table: mockTable,
335 },
336 }
337
338 const result = resolveEditCellConflicts(operations, editOperation)
339
340 expect(result.action).toBe('merge')
341 if (result.action === 'merge') {
342 expect(result.updatedOperations).toHaveLength(1)
343 const updatedAddRow = result.updatedOperations[0]
344 expect(updatedAddRow.type).toBe(QueuedOperationType.ADD_ROW)
345 expect((updatedAddRow.payload as any).rowData.name).toBe('edited value')
346 }
347 })
348
349 test('should return add action for normal edit on existing row', () => {
350 const otherOp: QueuedOperation = {
351 id: 'edit_cell_content:1:email:id:2',
352 type: QueuedOperationType.EDIT_CELL_CONTENT,
353 tableId: 1,
354 timestamp: Date.now(),
355 payload: {
356 rowIdentifiers: { id: 2 },
357 columnName: 'email',
358 oldValue: 'old@test.com',
359 newValue: 'new@test.com',
360 table: mockTable,
361 },
362 }
363
364 const operations = [otherOp]
365 const editOperation: NewEditCellContentOperation = {
366 type: QueuedOperationType.EDIT_CELL_CONTENT,
367 tableId: 1,
368 payload: {
369 rowIdentifiers: { id: 1 },
370 columnName: 'name',
371 oldValue: 'original',
372 newValue: 'changed',
373 table: mockTable,
374 },
375 }
376
377 const result = resolveEditCellConflicts(operations, editOperation)
378
379 expect(result.action).toBe('add')
380 })
381
382 test('should return add when editing tempId row but ADD_ROW not found', () => {
383 const operations: QueuedOperation[] = []
384 const editOperation: NewEditCellContentOperation = {
385 type: QueuedOperationType.EDIT_CELL_CONTENT,
386 tableId: 1,
387 payload: {
388 rowIdentifiers: { __tempId: '-99999' },
389 columnName: 'name',
390 oldValue: 'original',
391 newValue: 'changed',
392 table: mockTable,
393 },
394 }
395
396 const result = resolveEditCellConflicts(operations, editOperation)
397
398 expect(result.action).toBe('add')
399 })
400})
401
402describe('upsertOperation', () => {
403 const mockTable = {} as any
404
405 test('should add new operation to empty queue', () => {
406 const operations: QueuedOperation[] = []
407 const newOperation: NewEditCellContentOperation = {
408 type: QueuedOperationType.EDIT_CELL_CONTENT,
409 tableId: 1,
410 payload: {
411 rowIdentifiers: { id: 1 },
412 columnName: 'name',
413 oldValue: 'original',
414 newValue: 'changed',
415 table: mockTable,
416 },
417 }
418
419 const result = upsertOperation(operations, newOperation)
420
421 expect(result.operations).toHaveLength(1)
422 expect(result.operations[0].type).toBe(QueuedOperationType.EDIT_CELL_CONTENT)
423 expect(result.operations[0].id).toBe('edit_cell_content:1:name:id:1')
424 })
425
426 test('should add new operation to existing queue', () => {
427 const existingOp: QueuedOperation = {
428 id: 'edit_cell_content:1:email:id:1',
429 type: QueuedOperationType.EDIT_CELL_CONTENT,
430 tableId: 1,
431 timestamp: Date.now(),
432 payload: {
433 rowIdentifiers: { id: 1 },
434 columnName: 'email',
435 oldValue: 'old@test.com',
436 newValue: 'new@test.com',
437 table: mockTable,
438 },
439 }
440
441 const operations = [existingOp]
442 const newOperation: NewEditCellContentOperation = {
443 type: QueuedOperationType.EDIT_CELL_CONTENT,
444 tableId: 1,
445 payload: {
446 rowIdentifiers: { id: 1 },
447 columnName: 'name',
448 oldValue: 'original',
449 newValue: 'changed',
450 table: mockTable,
451 },
452 }
453
454 const result = upsertOperation(operations, newOperation)
455
456 expect(result.operations).toHaveLength(2)
457 })
458
459 test('should update existing EDIT_CELL operation and preserve original oldValue', () => {
460 const existingOp: QueuedOperation = {
461 id: 'edit_cell_content:1:name:id:1',
462 type: QueuedOperationType.EDIT_CELL_CONTENT,
463 tableId: 1,
464 timestamp: Date.now() - 1000,
465 payload: {
466 rowIdentifiers: { id: 1 },
467 columnName: 'name',
468 oldValue: 'very first value',
469 newValue: 'intermediate',
470 table: mockTable,
471 },
472 }
473
474 const operations = [existingOp]
475 const newOperation: NewEditCellContentOperation = {
476 type: QueuedOperationType.EDIT_CELL_CONTENT,
477 tableId: 1,
478 payload: {
479 rowIdentifiers: { id: 1 },
480 columnName: 'name',
481 oldValue: 'intermediate',
482 newValue: 'final value',
483 table: mockTable,
484 },
485 }
486
487 const result = upsertOperation(operations, newOperation)
488
489 expect(result.operations).toHaveLength(1)
490 const updated = result.operations[0]
491 expect((updated.payload as any).oldValue).toBe('very first value')
492 expect((updated.payload as any).newValue).toBe('final value')
493 })
494
495 test('should remove operation when value is reverted to original', () => {
496 const existingOp: QueuedOperation = {
497 id: 'edit_cell_content:1:name:id:1',
498 type: QueuedOperationType.EDIT_CELL_CONTENT,
499 tableId: 1,
500 timestamp: Date.now() - 1000,
501 payload: {
502 rowIdentifiers: { id: 1 },
503 columnName: 'name',
504 oldValue: 'original',
505 newValue: 'changed',
506 table: mockTable,
507 },
508 }
509
510 const otherOp: QueuedOperation = {
511 id: 'edit_cell_content:1:email:id:1',
512 type: QueuedOperationType.EDIT_CELL_CONTENT,
513 tableId: 1,
514 timestamp: Date.now(),
515 payload: {
516 rowIdentifiers: { id: 1 },
517 columnName: 'email',
518 oldValue: 'old@test.com',
519 newValue: 'new@test.com',
520 table: mockTable,
521 },
522 }
523
524 const operations = [existingOp, otherOp]
525 const newOperation: NewEditCellContentOperation = {
526 type: QueuedOperationType.EDIT_CELL_CONTENT,
527 tableId: 1,
528 payload: {
529 rowIdentifiers: { id: 1 },
530 columnName: 'name',
531 oldValue: 'changed',
532 newValue: 'original',
533 table: mockTable,
534 },
535 }
536
537 const result = upsertOperation(operations, newOperation)
538
539 expect(result.operations).toHaveLength(1)
540 expect(result.operations[0]).toEqual(otherOp)
541 })
542
543 test('should remove operation when number oldValue matches string newValue', () => {
544 const existingOp: QueuedOperation = {
545 id: 'edit_cell_content:1:age:id:1',
546 type: QueuedOperationType.EDIT_CELL_CONTENT,
547 tableId: 1,
548 timestamp: Date.now() - 1000,
549 payload: {
550 rowIdentifiers: { id: 1 },
551 columnName: 'age',
552 oldValue: 42,
553 newValue: 'changed',
554 table: mockTable,
555 },
556 }
557
558 const operations = [existingOp]
559 const newOperation: NewEditCellContentOperation = {
560 type: QueuedOperationType.EDIT_CELL_CONTENT,
561 tableId: 1,
562 payload: {
563 rowIdentifiers: { id: 1 },
564 columnName: 'age',
565 oldValue: 'changed',
566 newValue: '42',
567 table: mockTable,
568 },
569 }
570
571 const result = upsertOperation(operations, newOperation)
572
573 expect(result.operations).toHaveLength(0)
574 })
575
576 test('should remove operation when string oldValue matches JSON object newValue', () => {
577 const existingOp: QueuedOperation = {
578 id: 'edit_cell_content:1:metadata:id:1',
579 type: QueuedOperationType.EDIT_CELL_CONTENT,
580 tableId: 1,
581 timestamp: Date.now() - 1000,
582 payload: {
583 rowIdentifiers: { id: 1 },
584 columnName: 'metadata',
585 oldValue: '{"key":"value","count":3}',
586 newValue: 'changed',
587 table: mockTable,
588 },
589 }
590
591 const operations = [existingOp]
592 const newOperation: NewEditCellContentOperation = {
593 type: QueuedOperationType.EDIT_CELL_CONTENT,
594 tableId: 1,
595 payload: {
596 rowIdentifiers: { id: 1 },
597 columnName: 'metadata',
598 oldValue: 'changed',
599 newValue: { key: 'value', count: 3 },
600 table: mockTable,
601 },
602 }
603
604 const result = upsertOperation(operations, newOperation)
605
606 expect(result.operations).toHaveLength(0)
607 })
608
609 test('should update existing DELETE_ROW operation', () => {
610 const existingOp: QueuedOperation = {
611 id: 'delete_row:1:id:1',
612 type: QueuedOperationType.DELETE_ROW,
613 tableId: 1,
614 timestamp: Date.now() - 1000,
615 payload: {
616 rowIdentifiers: { id: 1 },
617 originalRow: { idx: 1, id: 1, name: 'old data' },
618 table: mockTable,
619 },
620 }
621
622 const operations = [existingOp]
623 const newOperation: NewDeleteRowOperation = {
624 type: QueuedOperationType.DELETE_ROW,
625 tableId: 1,
626 payload: {
627 rowIdentifiers: { id: 1 },
628 originalRow: { idx: 1, id: 1, name: 'updated data' },
629 table: mockTable,
630 },
631 }
632
633 const result = upsertOperation(operations, newOperation)
634
635 expect(result.operations).toHaveLength(1)
636 expect((result.operations[0].payload as any).originalRow.name).toBe('updated data')
637 })
638
639 test('should not mutate original operations array', () => {
640 const existingOp: QueuedOperation = {
641 id: 'edit_cell_content:1:name:id:1',
642 type: QueuedOperationType.EDIT_CELL_CONTENT,
643 tableId: 1,
644 timestamp: Date.now(),
645 payload: {
646 rowIdentifiers: { id: 1 },
647 columnName: 'name',
648 oldValue: 'original',
649 newValue: 'changed',
650 table: mockTable,
651 },
652 }
653
654 const operations = [existingOp]
655 const originalOperations = [...operations]
656 const newOperation: NewEditCellContentOperation = {
657 type: QueuedOperationType.EDIT_CELL_CONTENT,
658 tableId: 1,
659 payload: {
660 rowIdentifiers: { id: 2 },
661 columnName: 'name',
662 oldValue: 'original2',
663 newValue: 'changed2',
664 table: mockTable,
665 },
666 }
667
668 upsertOperation(operations, newOperation)
669
670 expect(operations).toEqual(originalOperations)
671 })
672
673 test('should handle ADD_ROW operation', () => {
674 const operations: QueuedOperation[] = []
675 const newOperation: NewAddRowOperation = {
676 type: QueuedOperationType.ADD_ROW,
677 tableId: 1,
678 payload: {
679 tempId: '-12345',
680 rowData: { idx: -12345, __tempId: '-12345', name: 'new row' },
681 table: mockTable,
682 },
683 }
684
685 const result = upsertOperation(operations, newOperation)
686
687 expect(result.operations).toHaveLength(1)
688 expect(result.operations[0].id).toBe('add_row:1:-12345')
689 })
690})