I’m trying to write a Vitest unit test for an auto-save feature in a Svelte 5 project.The test sets meta.settings.autoSaveIntervalMs = 50 so the save cycle finishes quickly, but the $effect still uses the old value 5000, nothing seems to be written to IndexedDB, and load() returns undefined, crashing the assertion.Below are the relevant file code.
model.ts
export interface ProjectMeta { title: string; settings: { autoSaveIntervalMs: number };}state.svelte.ts(reactive central state manager)
import { type ProjectMeta } from './model';import { persist, load } from './persistence';export const createProjectMeta = (title = 'Untitled'): ProjectMeta => ({ title, settings: { autoSaveIntervalMs: 5_000 },});export const meta = $state<ProjectMeta>(createProjectMeta());export function startAutoSave() { $effect(() => { // shallow copy captured once const dirty = { meta: { ...meta } }; const handle = setInterval(() => { persist(dirty); console.log('Autosaving', dirty); }, meta.settings.autoSaveIntervalMs); return () => clearInterval(handle); }); $inspect(meta);}persistence.ts(Dexie)
import Dexie from 'dexie';import type { ProjectMeta } from './model';interface Persisted { meta: ProjectMeta; }type PersistedSerialized = Persisted;const db = new (class extends Dexie { project!: Dexie.Table<PersistedSerialized, string>; constructor() { super('persist-on-interval'); this.version(1).stores({ project: '' }); }})();export const persist = (data: { meta: ProjectMeta }) => { console.log('About to store:', data); return db.project.put(data, 'main');};export const load = async () => { const raw = await db.project.get('main'); console.log('Raw from DB;', raw); return raw ?? null;};TestShell.svelte
<script> import { startAutoSave } from './state.svelte'; startAutoSave();</script>state.svelte.test.ts(the failing test)
import { describe, it, expect, beforeEach } from 'vitest';import { render, waitFor } from '@testing-library/svelte';import TestShell from './TestShell.svelte';import { meta } from './state.svelte';import { load } from './persistence';describe('State manager', () => { beforeEach(() => { Object.assign(meta, createProjectMeta()); }); it('persists on interval', async () => { // speed up the interval meta.settings.autoSaveIntervalMs = 50; render(TestShell); // starts the $effect await waitFor(() => document.body); meta.title = 'Renamed title for auto-save'; // wait at least one cycle await new Promise(r => setTimeout(r, 100)); const restored = await load(); expect(restored!.meta.title).toBe('Renamed title for auto-save'); });});Here's what observed in the console:
init { title: 'Untitled', settings: { autoSaveIntervalMs: 50 } }update { title: 'Renamed title for auto-save', settings: { autoSaveIntervalMs: 50 } }Autosaving { meta: { title: 'Renamed title for auto-save', settings: { autoSaveIntervalMs: 5000 } } }About to store: { meta: { title: 'Renamed title for auto-save', settings: { autoSaveIntervalMs: 5000 } } }Raw from DB: undefinedProblem summary
- In the test I set
meta.settings.autoSaveIntervalMs = 50. - The
$effectruns once, captures a shallow copy of meta, then schedulessetInterval(..., meta.settings.autoSaveIntervalMs /* 50 */). - Yet the very first autosave log shows
autoSaveIntervalMs: 5000again, proving the copy is stale. - After 100 ms,
load()returnsundefined, so the test explodes withTypeError: Cannot read properties of undefined (reading 'meta').
I have tried changing the timeout period in the test, waiting for TestShell to successfully mount (as seen in the test), etc. but still got back raw (in load()) as undefined.
My questions:
- Why doesn’t the
$effectre-run (and grab a fresh copy) whenmeta.settings.autoSaveIntervalMschanges? - Am I missing some Dexie/Vitest quirk that causes the store to come back
undefinedeven when data should have been written?
Any pointers would be greatly appreciated!