Files
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

232 lines
8.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @vitest-environment node
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
// Passthrough React hooks so the hook can run outside a React root.
vi.mock('react', () => ({
useCallback: (fn: unknown) => fn,
useEffect: (fn: () => void) => fn(),
useRef: (init: unknown) => ({ current: init }),
}))
const mockMutate = vi.fn()
const mockMutateAsync = vi.fn()
vi.mock('@/hooks/queries/tables', () => ({
useUpdateTableRow: vi.fn(() => ({ mutate: mockMutate })),
useCreateTableRow: vi.fn(() => ({ mutate: mockMutate })),
useBatchCreateTableRows: vi.fn(() => ({ mutate: mockMutate, mutateAsync: mockMutateAsync })),
useBatchUpdateTableRows: vi.fn(() => ({ mutate: mockMutate, mutateAsync: mockMutateAsync })),
useDeleteTableRow: vi.fn(() => ({ mutate: mockMutate })),
useDeleteTableRows: vi.fn(() => ({ mutate: mockMutate })),
useAddTableColumn: vi.fn(() => ({ mutate: mockMutate })),
useUpdateColumn: vi.fn(() => ({ mutate: mockMutate })),
useDeleteColumn: vi.fn(() => ({ mutate: mockMutate })),
useRenameTable: vi.fn(() => ({ mutate: mockMutate })),
useUpdateTableMetadata: vi.fn(() => ({ mutate: mockMutate })),
}))
vi.mock('@/lib/table/constants', () => ({
TABLE_LIMITS: { MAX_BULK_OPERATION_SIZE: 3 }, // small limit so tests don't need 1000 items
}))
const mockPopUndo = vi.fn()
const mockPopRedo = vi.fn()
const mockPush = vi.fn()
const mockPatchRedoRowId = vi.fn()
const mockPatchUndoRowId = vi.fn()
const mockClear = vi.fn()
const storeState = {
stacks: {},
push: mockPush,
popUndo: mockPopUndo,
popRedo: mockPopRedo,
patchRedoRowId: mockPatchRedoRowId,
patchUndoRowId: mockPatchUndoRowId,
clear: mockClear,
}
vi.mock('@/stores/table/store', () => ({
useTableUndoStore: vi.fn((selector: (s: typeof storeState) => unknown) => selector(storeState)),
runWithoutRecording: (fn: () => unknown) => Promise.resolve(fn()),
}))
import { useTableUndo } from '@/hooks/use-table-undo'
import type { TableUndoAction } from '@/stores/table/types'
const WORKSPACE_ID = 'ws-1'
const TABLE_ID = 'tbl-1'
function TestHook() {
return useTableUndo({ workspaceId: WORKSPACE_ID, tableId: TABLE_ID })
}
function makeEntry(action: TableUndoAction) {
return { id: 'e1', action, timestamp: Date.now() }
}
function makeCellsForClear(count: number) {
return Array.from({ length: count }, (_, i) => ({
rowId: `row-${i}`,
data: { col: `val-${i}` },
}))
}
/** Drain the microtask queue so all async chunks in executeAction finish. */
async function flush() {
await new Promise((r) => setTimeout(r, 0))
}
beforeEach(() => {
vi.clearAllMocks()
mockMutateAsync.mockResolvedValue({})
})
describe('useTableUndo clear-cells chunking (via undo)', () => {
it('sends a single mutateAsync call when cells fit in one chunk', async () => {
const cells = makeCellsForClear(2)
mockPopUndo.mockReturnValueOnce(makeEntry({ type: 'clear-cells', cells }))
const { undo } = TestHook()
;(undo as () => void)()
await flush()
expect(mockMutateAsync).toHaveBeenCalledTimes(1)
expect(mockMutateAsync.mock.calls[0][0].updates).toHaveLength(2)
})
it('splits into multiple chunks when cells exceed the limit', async () => {
const cells = makeCellsForClear(7) // limit=3 → [3,3,1]
mockPopUndo.mockReturnValueOnce(makeEntry({ type: 'clear-cells', cells }))
const { undo } = TestHook()
;(undo as () => void)()
await flush()
expect(mockMutateAsync).toHaveBeenCalledTimes(3)
expect(mockMutateAsync.mock.calls[0][0].updates).toHaveLength(3)
expect(mockMutateAsync.mock.calls[1][0].updates).toHaveLength(3)
expect(mockMutateAsync.mock.calls[2][0].updates).toHaveLength(1)
})
it('sends original data values for undo direction', async () => {
const cells = makeCellsForClear(1)
mockPopUndo.mockReturnValueOnce(makeEntry({ type: 'clear-cells', cells }))
const { undo } = TestHook()
;(undo as () => void)()
await flush()
expect(mockMutateAsync.mock.calls[0][0].updates[0].data.col).toBe('val-0')
})
it('sends null values for redo direction', async () => {
const cells = makeCellsForClear(1)
mockPopRedo.mockReturnValueOnce(makeEntry({ type: 'clear-cells', cells }))
const { redo } = TestHook()
;(redo as () => void)()
await flush()
expect(mockMutateAsync.mock.calls[0][0].updates[0].data.col).toBeNull()
})
it('does not call mutateAsync when cells is empty', async () => {
mockPopUndo.mockReturnValueOnce(makeEntry({ type: 'clear-cells', cells: [] }))
const { undo } = TestHook()
;(undo as () => void)()
await flush()
expect(mockMutateAsync).not.toHaveBeenCalled()
})
it('stops processing after the first failing chunk', async () => {
mockMutateAsync.mockRejectedValueOnce(new Error('Network error'))
const cells = makeCellsForClear(5) // limit=3 → would be [3,2] but stops at chunk 1
mockPopUndo.mockReturnValueOnce(makeEntry({ type: 'clear-cells', cells }))
const { undo } = TestHook()
// executeAction catches the error internally via logger — undo itself doesn't re-throw.
;(undo as () => void)()
await flush()
expect(mockMutateAsync).toHaveBeenCalledTimes(1)
})
})
describe('useTableUndo update-cells chunking (via undo)', () => {
function makeCellsForUpdate(count: number) {
return Array.from({ length: count }, (_, i) => ({
rowId: `row-${i}`,
oldData: { col: `old-${i}` },
newData: { col: `new-${i}` },
}))
}
it('sends a single call when cells fit within limit', async () => {
const cells = makeCellsForUpdate(2)
mockPopUndo.mockReturnValueOnce(makeEntry({ type: 'update-cells', cells }))
const { undo } = TestHook()
;(undo as () => void)()
await flush()
expect(mockMutateAsync).toHaveBeenCalledTimes(1)
expect(mockMutateAsync.mock.calls[0][0].updates[0].data.col).toBe('old-0')
})
it('chunks across multiple calls and picks the correct direction data', async () => {
const cells = makeCellsForUpdate(8) // limit=3 → [3,3,2]
mockPopRedo.mockReturnValueOnce(makeEntry({ type: 'update-cells', cells }))
const { redo } = TestHook()
;(redo as () => void)()
await flush()
expect(mockMutateAsync).toHaveBeenCalledTimes(3)
const lastChunk = mockMutateAsync.mock.calls[2][0].updates
expect(lastChunk).toHaveLength(2)
// redo direction → newData
expect(lastChunk[0].data.col).toBe('new-6')
})
})
describe('useTableUndo delete-column undo cell restore chunking', () => {
const baseAction: TableUndoAction = {
type: 'delete-column',
columnName: 'col',
columnType: 'string' as const,
columnPosition: 0,
columnUnique: false,
columnRequired: false,
cellData: [],
previousOrder: null,
previousWidth: null,
previousPinnedColumns: null,
}
it('does not call mutateAsync when cellData is empty', async () => {
mockPopUndo.mockReturnValueOnce(makeEntry(baseAction))
const { undo } = TestHook()
;(undo as () => void)()
await flush()
// addColumnMutation.mutate fires but the cell-restore block should not.
expect(mockMutateAsync).not.toHaveBeenCalled()
})
it('fires chunked mutateAsync calls via the onSuccess IIFE when cellData exceeds limit', async () => {
const cellData = Array.from({ length: 5 }, (_, i) => ({ rowId: `row-${i}`, value: i }))
const action: TableUndoAction = { ...baseAction, cellData }
mockPopUndo.mockReturnValueOnce(makeEntry(action))
// addColumnMutation.mutate is the first mockMutate call.
// Capture its onSuccess and invoke it to simulate column creation completing.
let capturedOnSuccess: (() => void) | undefined
mockMutate.mockImplementationOnce((_: unknown, opts: { onSuccess?: () => void }) => {
capturedOnSuccess = opts?.onSuccess
})
const { undo } = TestHook()
;(undo as () => void)()
await flush()
// At this point executeAction has returned, but the restore happens in the
// addColumn onSuccess callback — fire it now.
capturedOnSuccess?.()
// Allow the void IIFE's microtasks to drain.
await new Promise((r) => setTimeout(r, 0))
// limit=3 → [3, 2]
expect(mockMutateAsync).toHaveBeenCalledTimes(2)
expect(mockMutateAsync.mock.calls[0][0].updates).toHaveLength(3)
expect(mockMutateAsync.mock.calls[1][0].updates).toHaveLength(2)
})
})