Something went wrong. Try again.
Vibed Google Timeline alternative (VIBED)
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320import { afterEach, describe, expect, it, vi } from 'vitest'import type { LabelDay } from './api'import { setTimes } from './labels'import { LabelSession } from './labelSession.svelte'
const H = 3_600_000
function labelDay(date: string, extra: Partial<LabelDay> = {}): LabelDay { const start = Date.parse(`${date}T00:00:00Z`) return { date, zone: 'UTC', start, end: start + 24 * H, version: 1, savedAt: 1, seed: 'none', reviewed: false, items: [{ id: `s-${date}`, kind: 'stay', start: start + H, end: start + 2 * H }], complete: true, ...extra, }}
const json = (body: unknown, status = 200) => new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } })
/** A response the test releases when it wants. */function deferred() { let resolve!: (r: Response) => void const promise = new Promise<Response>((r) => (resolve = r)) return { promise, resolve }}
/** Routes fetch: GET /api/v1/labels lists nothing; other requests go to the handler in call order. */function serve(handler: (path: string, init: RequestInit) => Promise<Response>) { const fetch = vi.fn((path: string, init: RequestInit = {}) => path === '/api/v1/labels' ? Promise.resolve(json({ days: [] })) : handler(path, init), ) vi.stubGlobal('fetch', fetch) return fetch}
afterEach(() => vi.unstubAllGlobals())
describe('LabelSession', () => { it('ignores a save for a day no longer open', async () => { const a = labelDay('2026-06-01') const b = labelDay('2026-06-02') const put = deferred() serve((path, init) => (init.method === 'PUT' ? put.promise : Promise.resolve(json(path.endsWith(a.date) ? a : b)))) const s = new LabelSession() await s.load(a.date) s.setReviewed(true) const saving = s.save() await s.load(b.date) put.resolve(json({ ...a, version: 2, reviewed: true })) await saving expect(s.saved).toEqual(b) expect(s.items).toEqual(b.items) expect(s.saving).toBe(false) expect(s.dirty).toBe(false) })
it('ignores a failed save for a day no longer open', async () => { const a = labelDay('2026-06-01') const b = labelDay('2026-06-02') const put = deferred() serve((path, init) => (init.method === 'PUT' ? put.promise : Promise.resolve(json(path.endsWith(a.date) ? a : b)))) const s = new LabelSession() await s.load(a.date) s.setReviewed(true) const saving = s.save() await s.load(b.date) put.resolve(json({ error: 'conflict' }, 409)) await saving expect(s.server).toBeNull() })
it('keeps edits made while the day reloads', async () => { const a = labelDay('2026-06-01') const reload = deferred() let n = 0 serve(() => (++n === 1 ? Promise.resolve(json(a)) : reload.promise)) const s = new LabelSession() await s.load(a.date) const loading = s.load(a.date) s.setReviewed(true) reload.resolve(json({ ...a, version: 2 })) await loading expect(s.reviewed).toBe(true) expect(s.saved?.version).toBe(1) expect(s.dirty).toBe(true) })
it('applies a reload without edits, unless it is older than a save made meanwhile', async () => { const a = labelDay('2026-06-01') const reload = deferred() let n = 0 serve((_, init) => { if (init.method === 'PUT') return Promise.resolve(json({ ...a, version: 2, reviewed: true })) return ++n === 1 ? Promise.resolve(json(a)) : reload.promise }) const s = new LabelSession() await s.load(a.date) s.setReviewed(true) const loading = s.load(a.date) await s.save() reload.resolve(json(a)) await loading expect(s.saved?.version).toBe(2) expect(s.reviewed).toBe(true) })
it('ignores a save from an earlier opening of the day', async () => { const a = labelDay('2026-06-01') const b = labelDay('2026-06-02') const latest = labelDay(a.date, { version: 3, items: [{ ...a.items[0], note: 'saved elsewhere' }] }) const put = deferred() let aGets = 0 serve((path, init) => { if (init.method === 'PUT') return put.promise return Promise.resolve(json(path.endsWith(b.date) ? b : ++aGets === 1 ? a : latest)) }) const s = new LabelSession() await s.load(a.date) s.setReviewed(true) const saving = s.save() await s.load(b.date) await s.load(a.date) expect(s.saving).toBe(false) put.resolve(json({ ...a, version: 2, reviewed: true })) await saving expect(s.saved).toEqual(latest) expect(s.items).toEqual(latest.items) })
it('never goes back to an older version on save', async () => { const a = labelDay('2026-06-01') const put = deferred() let gets = 0 serve((_, init) => { if (init.method === 'PUT') return put.promise return Promise.resolve(json(++gets === 1 ? a : { ...a, version: 5 })) }) const s = new LabelSession() await s.load(a.date) s.setReviewed(true) const saving = s.save() // A reload of the same day (same opening) brings version 5 while the save is pending. await s.load(a.date) expect(s.saved?.version).toBe(5) put.resolve(json({ ...a, version: 2, reviewed: true })) await saving expect(s.saved?.version).toBe(5) expect(s.reviewed).toBe(false) })
it('keeps typed text started while the day reloads', async () => { const a = labelDay('2026-06-01') const reload = deferred() let n = 0 serve(() => (++n === 1 ? Promise.resolve(json(a)) : reload.promise)) const s = new LabelSession() await s.load(a.date) const loading = s.load(a.date) s.setDraft('start', true) reload.resolve(json({ ...a, version: 2, items: [] })) await loading expect(s.saved?.version).toBe(1) expect(s.items).toEqual(a.items) expect(s.dirty).toBe(true) })
it('keeps edits made while saving as unsaved edits on the new version', async () => { const a = labelDay('2026-06-01') const put = deferred() serve((_, init) => (init.method === 'PUT' ? put.promise : Promise.resolve(json(a)))) const s = new LabelSession() await s.load(a.date) s.setReviewed(true) const saving = s.save() const it = a.items[0] s.apply(setTimes(s.items, it.id, it.start!, it.end! + H)) put.resolve(json({ ...a, version: 2, reviewed: true })) await saving expect(s.saved?.version).toBe(2) expect(s.items[0].end).toBe(it.end! + H) expect(s.dirty).toBe(true) })
it('adds no undo step for an edit that changes nothing', async () => { const a = labelDay('2026-06-01') serve(() => Promise.resolve(json(a))) const s = new LabelSession() await s.load(a.date) const it = a.items[0] expect(s.apply(setTimes(s.items, it.id, it.start!, it.end!))).toBe(true) expect(s.canUndo).toBe(false) expect(s.selected).toBe(it.id) s.apply(setTimes(s.items, it.id, it.start!, it.end! + H)) expect(s.canUndo).toBe(true) })
it('counts typed text as unsaved and refuses to save it unapplied', async () => { const a = labelDay('2026-06-01') const fetch = serve(() => Promise.resolve(json(a))) const s = new LabelSession() await s.load(a.date) expect(s.dirty).toBe(false) s.setDraft('note', true) expect(s.dirty).toBe(true) await s.save() expect(s.editError).toMatch(/fix or clear it/) expect(fetch.mock.calls.some(([, init]) => init?.method === 'PUT')).toBe(false) s.setDraft('note', false) expect(s.dirty).toBe(false) })
it('drops edits on discard, even with a reload pending', async () => { const a = labelDay('2026-06-01') serve(() => Promise.resolve(json(a))) const s = new LabelSession() await s.load(a.date) s.setReviewed(true) await s.discard() expect(s.reviewed).toBe(false) expect(s.dirty).toBe(false) })
describe('a day whose data is incomplete', () => { const waiting = { reason: "Waiting for the reference phone's data after 14:04.", dataUntil: 1, lastUpload: 2 } const putBodies = (fetch: ReturnType<typeof serve>) => fetch.mock.calls.filter(([, init]) => init?.method === 'PUT').map(([, init]) => JSON.parse(init!.body as string))
it('opens read-only and refuses edits until unlocked', async () => { const a = labelDay('2026-06-01', { version: 0, complete: false, waiting }) const fetch = serve(() => Promise.resolve(json(a))) const s = new LabelSession() await s.load(a.date) expect(s.locked).toBe(true) s.setReviewed(true) const it = a.items[0] expect(s.apply(setTimes(s.items, it.id, it.start!, it.end! + H))).toBe(false) await s.save() expect(s.reviewed).toBe(false) expect(s.items).toEqual(a.items) expect(s.editError).toMatch(/Edit anyway/) expect(putBodies(fetch)).toEqual([]) })
it('saves with the override once unlocked, for that opening of the day only', async () => { const a = labelDay('2026-06-01', { version: 0, complete: false, waiting }) const b = labelDay('2026-06-02', { complete: false, waiting }) const fetch = serve((path, init) => { if (init.method === 'PUT') return Promise.resolve(json({ ...a, version: 1, reviewed: true, savedIncomplete: waiting })) return Promise.resolve(json(path.endsWith(a.date) ? a : b)) }) const s = new LabelSession() await s.load(a.date) s.unlock() expect(s.locked).toBe(false) s.setReviewed(true) await s.save() expect(putBodies(fetch)).toEqual([{ base: 0, reviewed: true, items: a.items, override: true }]) expect(s.saved?.savedIncomplete).toEqual(waiting) // Still incomplete, so still unlocked for more edits. expect(s.locked).toBe(false) await s.load(b.date) expect(s.locked).toBe(true) await s.load(a.date) expect(s.locked).toBe(true) })
it('sends no override for a complete day', async () => { const a = labelDay('2026-06-01') const fetch = serve((_, init) => Promise.resolve(json(init.method === 'PUT' ? { ...a, version: 2, reviewed: true } : a))) const s = new LabelSession() await s.load(a.date) expect(s.locked).toBe(false) s.setReviewed(true) await s.save() expect(putBodies(fetch)).toEqual([{ base: 1, reviewed: true, items: a.items }]) })
it('locks a day found incomplete on save, keeping the edits', async () => { const a = labelDay('2026-06-01') serve((_, init) => Promise.resolve(init.method === 'PUT' ? json({ error: 'incomplete', incomplete: true, waiting }, 409) : json(a)), ) const s = new LabelSession() await s.load(a.date) s.setReviewed(true) await s.save() expect(s.locked).toBe(true) expect(s.saved?.waiting).toEqual(waiting) expect(s.server?.conflict).toBeFalsy() expect(s.reviewed).toBe(true) expect(s.dirty).toBe(true) })
it('keeps a newer complete version loaded while a refused save was pending', async () => { const a = labelDay('2026-06-01') const put = deferred() let gets = 0 serve((_, init) => (init.method === 'PUT' ? put.promise : Promise.resolve(json(++gets === 1 ? a : { ...a, version: 5 })))) const s = new LabelSession() await s.load(a.date) s.setReviewed(true) const saving = s.save() await s.load(a.date) put.resolve(json({ error: 'incomplete', incomplete: true, waiting }, 409)) await saving expect(s.saved).toEqual({ ...a, version: 5 }) expect(s.locked).toBe(false) expect(s.server?.message).toMatch(/Not saved/) }) })})