queueOperationUtils.test.ts452 lines · main
1// @ts-nocheck
2import { describe, expect, test } from 'vitest'
3
4import type { SupaRow } from '../types'
5import {
6 formatGridDataWithOperationValues,
7 generateTableChangeKey,
8 rowMatchesIdentifiers,
9} from './queueOperationUtils'
10import {
11 QueuedOperationType,
12 type NewAddRowOperation,
13 type NewDeleteRowOperation,
14 type NewEditCellContentOperation,
15 type QueuedOperation,
16} from '@/state/table-editor-operation-queue.types'
17
18describe('generateTableChangeKey', () => {
19 test('should generate key for EDIT_CELL_CONTENT with row identifiers', () => {
20 const operation: NewEditCellContentOperation = {
21 type: QueuedOperationType.EDIT_CELL_CONTENT,
22 tableId: 1,
23 payload: {
24 rowIdentifiers: { id: 1 },
25 columnName: 'name',
26 oldValue: 'old',
27 newValue: 'new',
28 table: {} as any,
29 },
30 }
31 const key = generateTableChangeKey(operation)
32 expect(key).toBe('edit_cell_content:1:name:id:1')
33 })
34
35 test('should generate key for EDIT_CELL_CONTENT with empty row identifiers', () => {
36 const operation: NewEditCellContentOperation = {
37 type: QueuedOperationType.EDIT_CELL_CONTENT,
38 tableId: 1,
39 payload: {
40 rowIdentifiers: {},
41 columnName: 'name',
42 oldValue: 'old',
43 newValue: 'new',
44 table: {} as any,
45 },
46 }
47 const key = generateTableChangeKey(operation)
48 expect(key).toBe('edit_cell_content:1:name:')
49 })
50
51 test('should generate key with multiple row identifiers sorted alphabetically', () => {
52 const operation: NewEditCellContentOperation = {
53 type: QueuedOperationType.EDIT_CELL_CONTENT,
54 tableId: 1,
55 payload: {
56 rowIdentifiers: { z_id: 3, a_id: 1 },
57 columnName: 'name',
58 oldValue: 'old',
59 newValue: 'new',
60 table: {} as any,
61 },
62 }
63 const key = generateTableChangeKey(operation)
64 expect(key).toBe('edit_cell_content:1:name:a_id:1|z_id:3')
65 })
66
67 test('should generate key for ADD_ROW operation', () => {
68 const operation: NewAddRowOperation = {
69 type: QueuedOperationType.ADD_ROW,
70 tableId: 1,
71 payload: {
72 tempId: 'temp-123',
73 rowData: { idx: -1, __tempId: 'temp-123' },
74 table: {} as any,
75 },
76 }
77 const key = generateTableChangeKey(operation)
78 expect(key).toBe('add_row:1:temp-123')
79 })
80
81 test('should generate key for DELETE_ROW operation', () => {
82 const operation: NewDeleteRowOperation = {
83 type: QueuedOperationType.DELETE_ROW,
84 tableId: 1,
85 payload: {
86 rowIdentifiers: { id: 1 },
87 originalRow: { idx: 0, id: 1 },
88 table: {} as any,
89 },
90 }
91 const key = generateTableChangeKey(operation)
92 expect(key).toBe('delete_row:1:id:1')
93 })
94
95 test('should throw error for unknown operation type', () => {
96 const operation = {
97 type: 'unknown' as any,
98 tableId: 1,
99 payload: {
100 rowIdentifiers: { id: 1 },
101 columnName: 'name',
102 oldValue: 'old',
103 newValue: 'new',
104 table: {} as any,
105 },
106 }
107 expect(() => generateTableChangeKey(operation)).toThrow('Unknown operation type')
108 })
109})
110
111describe('rowMatchesIdentifiers', () => {
112 test('should return false for empty row identifiers', () => {
113 const result = rowMatchesIdentifiers({ id: 1 }, {})
114 expect(result).toBe(false)
115 })
116
117 test('should match row with single identifier', () => {
118 const result = rowMatchesIdentifiers({ id: 1 }, { id: 1 })
119 expect(result).toBe(true)
120 })
121
122 test('should match row with multiple identifiers', () => {
123 const result = rowMatchesIdentifiers(
124 { id: 1, email: 'test@test.com' },
125 { id: 1, email: 'test@test.com' }
126 )
127 expect(result).toBe(true)
128 })
129
130 test('should not match row with different values', () => {
131 const result = rowMatchesIdentifiers({ id: 2 }, { id: 1 })
132 expect(result).toBe(false)
133 })
134
135 test('should not match row with missing identifier keys', () => {
136 const result = rowMatchesIdentifiers({ id: 1 }, { id: 1, email: 'test@test.com' })
137 expect(result).toBe(false)
138 })
139
140 test('should match row with extra keys', () => {
141 const result = rowMatchesIdentifiers({ id: 1, name: 'John', age: 30 }, { id: 1 })
142 expect(result).toBe(true)
143 })
144
145 test('should match with null values', () => {
146 const result = rowMatchesIdentifiers({ id: null }, { id: null })
147 expect(result).toBe(true)
148 })
149
150 test('should not match with undefined values in row', () => {
151 const result = rowMatchesIdentifiers({ id: undefined, name: 'test' }, { id: 1 })
152 expect(result).toBe(false)
153 })
154})
155
156describe('formatGridDataWithOperationValues', () => {
157 const makeRow = (idx: number, data: Record<string, unknown> = {}): SupaRow => ({
158 idx,
159 ...data,
160 })
161
162 const makeEditOp = (
163 overrides: Partial<QueuedOperation & { payload: any }> = {}
164 ): QueuedOperation => ({
165 id: 'op-1',
166 tableId: 1,
167 timestamp: Date.now(),
168 type: QueuedOperationType.EDIT_CELL_CONTENT,
169 payload: {
170 rowIdentifiers: { id: 1 },
171 columnName: 'name',
172 oldValue: 'old',
173 newValue: 'new',
174 table: {} as any,
175 },
176 ...overrides,
177 })
178
179 const makeDeleteOp = (
180 rowIdentifiers: Record<string, unknown>,
181 originalRow: SupaRow
182 ): QueuedOperation => ({
183 id: 'op-del',
184 tableId: 1,
185 timestamp: Date.now(),
186 type: QueuedOperationType.DELETE_ROW,
187 payload: { rowIdentifiers, originalRow, table: {} as any },
188 })
189
190 const makeAddOp = (tempId: string, rowData: Record<string, unknown> = {}): QueuedOperation => ({
191 id: 'op-add',
192 tableId: 1,
193 timestamp: Date.now(),
194 type: QueuedOperationType.ADD_ROW,
195 payload: {
196 tempId,
197 rowData: { idx: Number(tempId), __tempId: tempId, ...rowData },
198 table: {} as any,
199 },
200 })
201
202 test('should return rows unchanged when there are no operations', () => {
203 const rows = [makeRow(0, { id: 1, name: 'Alice' }), makeRow(1, { id: 2, name: 'Bob' })]
204 const result = formatGridDataWithOperationValues({ operations: [], rows })
205 expect(result).toEqual(rows)
206 })
207
208 test('should apply EDIT_CELL_CONTENT to matching row', () => {
209 const rows = [makeRow(0, { id: 1, name: 'Alice' }), makeRow(1, { id: 2, name: 'Bob' })]
210 const op = makeEditOp({
211 payload: {
212 rowIdentifiers: { id: 1 },
213 columnName: 'name',
214 oldValue: 'Alice',
215 newValue: 'Updated',
216 table: {} as any,
217 },
218 })
219
220 const result = formatGridDataWithOperationValues({ operations: [op], rows })
221 expect(result[0]).toEqual({ idx: 0, id: 1, name: 'Updated' })
222 expect(result[1]).toEqual(rows[1])
223 })
224
225 test('should not modify rows when EDIT_CELL_CONTENT does not match any row', () => {
226 const rows = [makeRow(0, { id: 1, name: 'Alice' })]
227 const op = makeEditOp({
228 payload: {
229 rowIdentifiers: { id: 999 },
230 columnName: 'name',
231 oldValue: 'old',
232 newValue: 'new',
233 table: {} as any,
234 },
235 })
236
237 const result = formatGridDataWithOperationValues({ operations: [op], rows })
238 expect(result).toEqual(rows)
239 })
240
241 test('should apply multiple EDIT_CELL_CONTENT operations to different rows', () => {
242 const rows = [makeRow(0, { id: 1, name: 'Alice' }), makeRow(1, { id: 2, name: 'Bob' })]
243 const op1 = makeEditOp({
244 id: 'op-1',
245 payload: {
246 rowIdentifiers: { id: 1 },
247 columnName: 'name',
248 oldValue: 'Alice',
249 newValue: 'Updated Alice',
250 table: {} as any,
251 },
252 })
253 const op2 = makeEditOp({
254 id: 'op-2',
255 payload: {
256 rowIdentifiers: { id: 2 },
257 columnName: 'name',
258 oldValue: 'Bob',
259 newValue: 'Updated Bob',
260 table: {} as any,
261 },
262 })
263
264 const result = formatGridDataWithOperationValues({ operations: [op1, op2], rows })
265 expect(result[0].name).toBe('Updated Alice')
266 expect(result[1].name).toBe('Updated Bob')
267 })
268
269 test('multiple operations targeting the same row preserve all column changes', () => {
270 const rows = [makeRow(0, { id: 1, name: 'Alice', email: 'alice@test.com' })]
271 const op1 = makeEditOp({
272 id: 'op-1',
273 payload: {
274 rowIdentifiers: { id: 1 },
275 columnName: 'name',
276 oldValue: 'Alice',
277 newValue: 'Updated',
278 table: {} as any,
279 },
280 })
281 const op2 = makeEditOp({
282 id: 'op-2',
283 payload: {
284 rowIdentifiers: { id: 1 },
285 columnName: 'email',
286 oldValue: 'alice@test.com',
287 newValue: 'updated@test.com',
288 table: {} as any,
289 },
290 })
291
292 const result = formatGridDataWithOperationValues({ operations: [op1, op2], rows })
293 // Both column edits should be preserved
294 expect(result[0].name).toBe('Updated')
295 expect(result[0].email).toBe('updated@test.com')
296 })
297
298 test('should mark matching row as deleted for DELETE_ROW operation', () => {
299 const rows = [makeRow(0, { id: 1, name: 'Alice' }), makeRow(1, { id: 2, name: 'Bob' })]
300 const op = makeDeleteOp({ id: 1 }, rows[0])
301
302 const result = formatGridDataWithOperationValues({ operations: [op], rows })
303 expect(result[0].__isDeleted).toBe(true)
304 expect(result[1].__isDeleted).toBeUndefined()
305 })
306
307 test('should not modify rows when DELETE_ROW does not match any row', () => {
308 const rows = [makeRow(0, { id: 1, name: 'Alice' })]
309 const op = makeDeleteOp({ id: 999 }, makeRow(0, { id: 999 }))
310
311 const result = formatGridDataWithOperationValues({ operations: [op], rows })
312 expect(result[0].__isDeleted).toBeUndefined()
313 })
314
315 test('should not mutate the original rows array', () => {
316 const rows = [makeRow(0, { id: 1, name: 'Alice' })]
317 const op = makeEditOp({
318 payload: {
319 rowIdentifiers: { id: 1 },
320 columnName: 'name',
321 oldValue: 'Alice',
322 newValue: 'Updated',
323 table: {} as any,
324 },
325 })
326
327 const result = formatGridDataWithOperationValues({ operations: [op], rows })
328 expect(rows[0].name).toBe('Alice')
329 expect(result[0].name).toBe('Updated')
330 })
331
332 test('should handle mixed operation types', () => {
333 const rows = [
334 makeRow(0, { id: 1, name: 'Alice' }),
335 makeRow(1, { id: 2, name: 'Bob' }),
336 makeRow(2, { id: 3, name: 'Charlie' }),
337 ]
338
339 const editOp = makeEditOp({
340 payload: {
341 rowIdentifiers: { id: 1 },
342 columnName: 'name',
343 oldValue: 'Alice',
344 newValue: 'Updated Alice',
345 table: {} as any,
346 },
347 })
348 const deleteOp = makeDeleteOp({ id: 2 }, rows[1])
349
350 const result = formatGridDataWithOperationValues({
351 operations: [editOp, deleteOp],
352 rows,
353 })
354
355 expect(result[0].name).toBe('Updated Alice')
356 expect(result[1].__isDeleted).toBe(true)
357 expect(result[2]).toEqual(rows[2])
358 })
359
360 test('should prepend new row for ADD_ROW operation', () => {
361 const rows = [makeRow(0, { id: 1, name: 'Alice' })]
362 const op = makeAddOp('-100', { name: 'New Row' })
363
364 const result = formatGridDataWithOperationValues({ operations: [op], rows })
365 expect(result).toHaveLength(2)
366 expect(result[0]).toMatchObject({ __tempId: '-100', name: 'New Row' })
367 expect(result[1]).toEqual(rows[0])
368 })
369
370 test('should update existing pending row for ADD_ROW with same tempId', () => {
371 const rows: SupaRow[] = [
372 { idx: -100, __tempId: '-100', name: 'Original' } as any,
373 makeRow(1, { id: 1, name: 'Alice' }),
374 ]
375 const op = makeAddOp('-100', { name: 'Updated' })
376
377 const result = formatGridDataWithOperationValues({ operations: [op], rows })
378 expect(result).toHaveLength(2)
379 expect(result[0]).toMatchObject({ __tempId: '-100', name: 'Updated' })
380 })
381
382 test('should handle multiple ADD_ROW operations', () => {
383 const rows = [makeRow(0, { id: 1, name: 'Alice' })]
384 const op1 = makeAddOp('-100', { name: 'Row 1' })
385 const op2 = makeAddOp('-200', { name: 'Row 2' })
386
387 const result = formatGridDataWithOperationValues({ operations: [op1, op2], rows })
388 expect(result).toHaveLength(3)
389 expect(result[0]).toMatchObject({ __tempId: '-200', name: 'Row 2' })
390 expect(result[1]).toMatchObject({ __tempId: '-100', name: 'Row 1' })
391 })
392
393 test('should correctly delete a row after adding a new row (ADD then DELETE)', () => {
394 const rows = [makeRow(0, { id: 1, name: 'Alice' }), makeRow(1, { id: 2, name: 'Bob' })]
395 const addOp = makeAddOp('-100', { name: 'New Row' })
396 const deleteOp = makeDeleteOp({ id: 1 }, rows[0])
397
398 const result = formatGridDataWithOperationValues({
399 operations: [addOp, deleteOp],
400 rows,
401 })
402
403 expect(result).toHaveLength(3)
404 // New row should be preserved at position 0
405 expect(result[0]).toMatchObject({ __tempId: '-100', name: 'New Row' })
406 expect(result[0].__isDeleted).toBeUndefined()
407 // Deleted row should be marked
408 expect(result[1].__isDeleted).toBe(true)
409 expect(result[1].id).toBe(1)
410 // Other row unaffected
411 expect(result[2]).toEqual(rows[1])
412 })
413
414 test('should correctly edit a row after adding a new row (ADD then EDIT)', () => {
415 const rows = [makeRow(0, { id: 1, name: 'Alice' })]
416 const addOp = makeAddOp('-100', { name: 'New Row' })
417 const editOp = makeEditOp({
418 payload: {
419 rowIdentifiers: { id: 1 },
420 columnName: 'name',
421 oldValue: 'Alice',
422 newValue: 'Updated Alice',
423 table: {} as any,
424 },
425 })
426
427 const result = formatGridDataWithOperationValues({
428 operations: [addOp, editOp],
429 rows,
430 })
431
432 expect(result).toHaveLength(2)
433 expect(result[0]).toMatchObject({ __tempId: '-100', name: 'New Row' })
434 expect(result[1].name).toBe('Updated Alice')
435 })
436
437 test('should handle EDIT_CELL_CONTENT with composite primary keys', () => {
438 const rows = [makeRow(0, { tenant_id: 'a', user_id: 1, name: 'Alice' })]
439 const op = makeEditOp({
440 payload: {
441 rowIdentifiers: { tenant_id: 'a', user_id: 1 },
442 columnName: 'name',
443 oldValue: 'Alice',
444 newValue: 'Updated',
445 table: {} as any,
446 },
447 })
448
449 const result = formatGridDataWithOperationValues({ operations: [op], rows })
450 expect(result[0].name).toBe('Updated')
451 })
452})