fix(canvas): guard autosave against design-switch clobber, add tests

Extract the debounced autosave into a useAutosave hook. Pin the active
design id when the timer is armed and re-check it at fire time: if the
user switched designs while the timer was pending, the in-memory canvas
belongs to a different design, so skip the save instead of writing it
under the wrong design id. Also skip when there is no active design.

Add unit tests for autosaveSettings (persistence, validation, pub/sub)
and useAutosave (debounce, enable/unsaved/design guards, switch skip).

ha-relevant: maybe
This commit is contained in:
Pouzor
2026-07-17 11:16:56 +02:00
parent a7b7327ce5
commit 310b9cb3fd
4 changed files with 229 additions and 7 deletions
@@ -0,0 +1,58 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import {
DEFAULT_AUTOSAVE_SETTINGS,
readAutosaveSettings,
writeAutosaveSettings,
subscribeAutosaveSettings,
} from '../autosaveSettings'
describe('autosaveSettings', () => {
beforeEach(() => {
localStorage.clear()
})
it('defaults to disabled with a 5s delay', () => {
expect(DEFAULT_AUTOSAVE_SETTINGS).toEqual({ enabled: false, delay: 5 })
})
it('returns defaults when nothing stored', () => {
expect(readAutosaveSettings()).toEqual(DEFAULT_AUTOSAVE_SETTINGS)
})
it('roundtrips through localStorage', () => {
writeAutosaveSettings({ enabled: true, delay: 30 })
expect(readAutosaveSettings()).toEqual({ enabled: true, delay: 30 })
})
it('falls back to defaults when stored value is corrupted', () => {
localStorage.setItem('homelable.autosave', '{not json')
expect(readAutosaveSettings()).toEqual(DEFAULT_AUTOSAVE_SETTINGS)
})
it('fills missing fields from defaults', () => {
localStorage.setItem('homelable.autosave', JSON.stringify({ enabled: true }))
expect(readAutosaveSettings()).toEqual({ enabled: true, delay: DEFAULT_AUTOSAVE_SETTINGS.delay })
})
it('rejects a non-positive delay and falls back to default', () => {
localStorage.setItem('homelable.autosave', JSON.stringify({ enabled: true, delay: 0 }))
expect(readAutosaveSettings().delay).toBe(DEFAULT_AUTOSAVE_SETTINGS.delay)
localStorage.setItem('homelable.autosave', JSON.stringify({ enabled: true, delay: -10 }))
expect(readAutosaveSettings().delay).toBe(DEFAULT_AUTOSAVE_SETTINGS.delay)
})
it('rejects a non-numeric delay and falls back to default', () => {
localStorage.setItem('homelable.autosave', JSON.stringify({ enabled: true, delay: 'soon' }))
expect(readAutosaveSettings().delay).toBe(DEFAULT_AUTOSAVE_SETTINGS.delay)
})
it('notifies subscribers on write and stops after unsubscribe', () => {
const listener = vi.fn()
const unsubscribe = subscribeAutosaveSettings(listener)
writeAutosaveSettings({ enabled: true, delay: 10 })
expect(listener).toHaveBeenCalledWith({ enabled: true, delay: 10 })
unsubscribe()
writeAutosaveSettings({ enabled: false, delay: 3 })
expect(listener).toHaveBeenCalledTimes(1)
})
})