import { afterEach, describe, expect, it, vi } from 'vitest'; import { HttpSnapshotStore } from '../../src/core/store/HttpSnapshotStore'; import { StaleRevisionError } from '../../src/core/store/ProjectStore'; import { snapshotBlocker, snapshotTime } from '../../src/editor/snapshots'; function stubFetch(body: BodyInit | null, init: ResponseInit) { const mock = vi.fn(() => Promise.resolve(new Response(body, init))); vi.stubGlobal('fetch', mock); return mock; } afterEach(() => { vi.unstubAllGlobals(); }); describe('snapshot rules (#231)', () => { it('shows the time in local time, minutes padded', () => { expect(snapshotTime(new Date(2026, 0, 5, 7, 3))).toBe('2026-01-05 07:03'); }); it('allows a snapshot only when everything is saved', () => { expect(snapshotBlocker('saved')).toBeNull(); for (const status of ['dirty', 'saving', 'error', 'conflict'] as const) { expect(snapshotBlocker(status)).toEqual(expect.any(String)); } expect(snapshotBlocker('conflict')).toContain('changed elsewhere'); }); }); describe('HttpSnapshotStore (#231)', () => { it('lists, creates and deletes under the project', async () => { const mock = stubFetch(JSON.stringify([]), { status: 200 }); const store = new HttpSnapshotStore('/api'); await store.list('p1'); await store.create('p1', 'before the boss'); stubFetch(null, { status: 204 }); await store.delete('p1', 's1'); const calls = [...mock.mock.calls, ...vi.mocked(fetch).mock.calls].map(([url, init]) => [ decodeURIComponent((url as string).split('r=')[1] ?? ''), init?.method, init?.body, ]); expect(calls).toEqual([ ['projects/p1/snapshots', 'GET', undefined], ['projects/p1/snapshots', 'POST', JSON.stringify({ name: 'before the boss' })], ['projects/p1/snapshots/s1', 'DELETE', undefined], ]); }); it('restores with the revision the editor holds and hands back the kept stand', async () => { const before = { id: 's2', name: 'Before restoring a', revision: 7, createdAt: '' }; const mock = stubFetch(JSON.stringify({ revision: 8, before }), { status: 200 }); const result = await new HttpSnapshotStore('/api').restore('p1', 's1', 7); expect(result).toEqual({ revision: 8, before }); expect(decodeURIComponent(mock.mock.calls[0]?.[0] as string)).toContain( 'r=projects/p1/snapshots/s1/restore', ); expect(mock.mock.calls[0]?.[1]?.body).toBe(JSON.stringify({ revision: 7 })); }); it('turns a 409 into a stale revision', async () => { stubFetch(JSON.stringify({ error: 'Project was modified elsewhere', revision: 9 }), { status: 409 }); const failure = await new HttpSnapshotStore('/api').restore('p1', 's1', 7).catch((e: unknown) => e); expect(failure).toBeInstanceOf(StaleRevisionError); expect((failure as StaleRevisionError).serverRevision).toBe(9); }); });