8d752e0bd8
The previous guard compared the pinned design against the live activeDesignId (the selection). On a design switch the selection flips synchronously while the new canvas loads asynchronously, so the on-screen nodes briefly still belong to the previous design — arming the timer with the newly-selected id would save those stale nodes under the wrong design. Track the design the canvas was actually loaded as (loadedDesignId provenance) and gate autosave on that instead: pin it when the timer is armed and re-check the live provenance at fire time, skipping the save if a different canvas has since loaded. Also validate the persisted `enabled` flag by type (mirror of `delay`), and add the matching regression tests (provenance switch skip, enabled type validation). ha-relevant: maybe
98 lines
3.3 KiB
TypeScript
98 lines
3.3 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
import { renderHook } from '@testing-library/react'
|
|
import { useAutosave } from '../useAutosave'
|
|
|
|
interface Args {
|
|
enabled?: boolean
|
|
delaySeconds?: number
|
|
hasUnsavedChanges?: boolean
|
|
designId?: string | null
|
|
changeSignals?: unknown[]
|
|
getLiveDesignId?: () => string | null
|
|
onSave?: (designId: string) => void
|
|
}
|
|
|
|
function args(over: Args = {}) {
|
|
const designId = 'designId' in over ? (over.designId ?? null) : 'design-a'
|
|
return {
|
|
enabled: over.enabled ?? true,
|
|
delaySeconds: over.delaySeconds ?? 5,
|
|
hasUnsavedChanges: over.hasUnsavedChanges ?? true,
|
|
designId,
|
|
changeSignals: over.changeSignals ?? [[], []],
|
|
getLiveDesignId: over.getLiveDesignId ?? (() => designId),
|
|
onSave: over.onSave ?? vi.fn(),
|
|
}
|
|
}
|
|
|
|
describe('useAutosave', () => {
|
|
beforeEach(() => vi.useFakeTimers())
|
|
afterEach(() => vi.useRealTimers())
|
|
|
|
it('saves after the inactivity delay when enabled with unsaved changes', () => {
|
|
const onSave = vi.fn()
|
|
renderHook(() => useAutosave(args({ onSave, delaySeconds: 5 })))
|
|
expect(onSave).not.toHaveBeenCalled()
|
|
vi.advanceTimersByTime(4999)
|
|
expect(onSave).not.toHaveBeenCalled()
|
|
vi.advanceTimersByTime(1)
|
|
expect(onSave).toHaveBeenCalledTimes(1)
|
|
expect(onSave).toHaveBeenCalledWith('design-a')
|
|
})
|
|
|
|
it('does nothing when disabled', () => {
|
|
const onSave = vi.fn()
|
|
renderHook(() => useAutosave(args({ onSave, enabled: false })))
|
|
vi.advanceTimersByTime(60_000)
|
|
expect(onSave).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('does nothing when there are no unsaved changes', () => {
|
|
const onSave = vi.fn()
|
|
renderHook(() => useAutosave(args({ onSave, hasUnsavedChanges: false })))
|
|
vi.advanceTimersByTime(60_000)
|
|
expect(onSave).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('does nothing when the canvas has no known provenance', () => {
|
|
const onSave = vi.fn()
|
|
renderHook(() => useAutosave(args({ onSave, designId: null })))
|
|
vi.advanceTimersByTime(60_000)
|
|
expect(onSave).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('debounces: a change before the delay resets the timer', () => {
|
|
const onSave = vi.fn()
|
|
const { rerender } = renderHook((p: Args) => useAutosave(args(p)), {
|
|
initialProps: { onSave, changeSignals: [1] },
|
|
})
|
|
vi.advanceTimersByTime(4000)
|
|
rerender({ onSave, changeSignals: [2] }) // edit resets debounce
|
|
vi.advanceTimersByTime(4000)
|
|
expect(onSave).not.toHaveBeenCalled()
|
|
vi.advanceTimersByTime(1000)
|
|
expect(onSave).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('skips the save if a different canvas loaded while the timer was pending', () => {
|
|
const onSave = vi.fn()
|
|
// Pinned provenance at arm time = 'design-a', but a switch loaded 'design-b'
|
|
// before the timer fired — the on-screen canvas is no longer design-a's.
|
|
renderHook(() =>
|
|
useAutosave(args({ onSave, designId: 'design-a', getLiveDesignId: () => 'design-b' })),
|
|
)
|
|
vi.advanceTimersByTime(5000)
|
|
expect(onSave).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('saves under the pinned provenance id when it still matches at fire time', () => {
|
|
const onSave = vi.fn()
|
|
renderHook(() =>
|
|
useAutosave(args({ onSave, designId: 'design-a', getLiveDesignId: () => 'design-a' })),
|
|
)
|
|
vi.advanceTimersByTime(5000)
|
|
expect(onSave).toHaveBeenCalledTimes(1)
|
|
expect(onSave).toHaveBeenCalledWith('design-a')
|
|
})
|
|
})
|