test: cover autosave controller and lifecycle helper
This commit is contained in:
parent
7b5af20dee
commit
6b21c4f7b3
2 changed files with 159 additions and 0 deletions
60
src/lib/admin/autoSaveLifecycle.ts
Normal file
60
src/lib/admin/autoSaveLifecycle.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { beforeNavigate } from '$app/navigation'
|
||||
import { onDestroy } from 'svelte'
|
||||
import type { AutoSaveController } from './autoSave'
|
||||
|
||||
interface AutoSaveLifecycleOptions {
|
||||
isReady?: () => boolean
|
||||
onFlushError?: (error: unknown) => void
|
||||
enableShortcut?: boolean
|
||||
}
|
||||
|
||||
export function initAutoSaveLifecycle(
|
||||
controller: AutoSaveController,
|
||||
options: AutoSaveLifecycleOptions = {}
|
||||
) {
|
||||
const { isReady = () => true, onFlushError, enableShortcut = true } = options
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
onDestroy(() => controller.destroy())
|
||||
return
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (!enableShortcut) return
|
||||
if (!isReady()) return
|
||||
const key = event.key.toLowerCase()
|
||||
const isModifier = event.metaKey || event.ctrlKey
|
||||
if (!isModifier || key !== 's') return
|
||||
event.preventDefault()
|
||||
controller.flush().catch((error) => {
|
||||
onFlushError?.(error)
|
||||
})
|
||||
}
|
||||
|
||||
if (enableShortcut) {
|
||||
document.addEventListener('keydown', handleKeydown)
|
||||
}
|
||||
|
||||
const stopNavigating = beforeNavigate(async (navigation) => {
|
||||
if (!isReady()) return
|
||||
navigation.cancel()
|
||||
try {
|
||||
await controller.flush()
|
||||
navigation.retry()
|
||||
} catch (error) {
|
||||
onFlushError?.(error)
|
||||
}
|
||||
})
|
||||
|
||||
const stop = () => {
|
||||
if (enableShortcut) {
|
||||
document.removeEventListener('keydown', handleKeydown)
|
||||
}
|
||||
stopNavigating?.()
|
||||
controller.destroy()
|
||||
}
|
||||
|
||||
onDestroy(stop)
|
||||
|
||||
return { stop }
|
||||
}
|
||||
99
tests/autoSaveController.test.ts
Normal file
99
tests/autoSaveController.test.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import { describe, it, beforeEach } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { createAutoSaveController } from '../src/lib/admin/autoSave'
|
||||
|
||||
describe('createAutoSaveController', () => {
|
||||
beforeEach(() => {
|
||||
if (typeof navigator === 'undefined') {
|
||||
// @ts-expect-error add minimal navigator shim for tests
|
||||
global.navigator = { onLine: true }
|
||||
}
|
||||
})
|
||||
|
||||
it('skips save when payload matches primed baseline', async () => {
|
||||
let value = 0
|
||||
let saveCalls = 0
|
||||
|
||||
const controller = createAutoSaveController<{ value: number }>({
|
||||
debounceMs: 5,
|
||||
getPayload: () => ({ value }),
|
||||
save: async () => {
|
||||
saveCalls += 1
|
||||
return { value }
|
||||
}
|
||||
})
|
||||
|
||||
controller.prime({ value })
|
||||
controller.schedule()
|
||||
|
||||
await wait(15)
|
||||
assert.equal(saveCalls, 0)
|
||||
|
||||
await controller.flush()
|
||||
assert.equal(saveCalls, 0)
|
||||
|
||||
controller.destroy()
|
||||
})
|
||||
|
||||
it('saves when payload changes and returns to idle after success', async () => {
|
||||
let value = 0
|
||||
let updatedAt = 0
|
||||
let saveCalls = 0
|
||||
const statuses: string[] = []
|
||||
|
||||
const controller = createAutoSaveController<{ value: number; updatedAt: number }, { updatedAt: number }>({
|
||||
debounceMs: 5,
|
||||
idleResetMs: 10,
|
||||
getPayload: () => ({ value, updatedAt }),
|
||||
save: async (payload) => {
|
||||
saveCalls += 1
|
||||
return { updatedAt: payload.updatedAt + 1 }
|
||||
},
|
||||
onSaved: (response, { prime }) => {
|
||||
updatedAt = response.updatedAt
|
||||
prime({ value, updatedAt })
|
||||
}
|
||||
})
|
||||
|
||||
const unsubscribe = controller.status.subscribe((status) => {
|
||||
statuses.push(status)
|
||||
})
|
||||
|
||||
controller.prime({ value, updatedAt })
|
||||
|
||||
value = 1
|
||||
controller.schedule()
|
||||
|
||||
await wait(15)
|
||||
assert.equal(saveCalls, 1)
|
||||
assert.ok(statuses.includes('saving'))
|
||||
assert.ok(statuses.includes('saved'))
|
||||
|
||||
await wait(20)
|
||||
assert.equal(statuses.at(-1), 'idle')
|
||||
|
||||
unsubscribe()
|
||||
controller.destroy()
|
||||
})
|
||||
|
||||
it('cancels pending work on destroy', async () => {
|
||||
let saveCalls = 0
|
||||
const controller = createAutoSaveController<{ foo: string }>({
|
||||
debounceMs: 20,
|
||||
getPayload: () => ({ foo: 'bar' }),
|
||||
save: async () => {
|
||||
saveCalls += 1
|
||||
}
|
||||
})
|
||||
|
||||
controller.schedule()
|
||||
controller.destroy()
|
||||
|
||||
await wait(30)
|
||||
assert.equal(saveCalls, 0)
|
||||
})
|
||||
})
|
||||
|
||||
function wait(ms: number) {
|
||||
return new Promise<void>((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
Loading…
Reference in a new issue