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: undefined
Problem 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!
ProjectMetais serializable). However, it remains unclear whyautoSaveIntervalMsmysteriously switched back to 5000 when it was about to be auto-saved, as if the Svelte state rune had completely forgotten the change.