Files
MindSift/frontend/src/lib/__tests__/offline.test.ts
T
sandrodachandClaude Opus 5.5 7bba174454 feat(pwa): start and keep working offline
The service worker now caches the app shell: page loads are network
first with the last index.html as fallback, hashed bundles come from the
cache, icons and manifest are refreshed in the background; API calls are
never intercepted. GET answers are kept per user in IndexedDB and served
when the server cannot be reached, so the app starts logged in with the
last known thoughts, tasks and lists. Captured thoughts and checked tasks
or list items go to an outbox that is sent in order when the connection
returns (also at start and every 30 s). A banner shows offline state and
waiting changes. Logging out clears cache and outbox.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 08:11:47 +01:00

50 lines
2.0 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest';
import { enqueue, flushOutbox, getOutbox, isCacheablePath } from '../offline';
const res = (status: number) => ({ status, ok: status >= 200 && status < 300 }) as Response;
describe('outbox', () => {
beforeEach(() => localStorage.clear());
it('keeps only the last state of repeated toggles', () => {
enqueue('PATCH', '/api/v1/thoughts/t1', { isCompleted: true });
enqueue('POST', '/api/v1/inbox', { content: 'Milch' });
enqueue('PATCH', '/api/v1/thoughts/t1', { isCompleted: false });
const box = getOutbox();
expect(box).toHaveLength(2);
expect(box[0].body).toEqual({ isCompleted: false });
});
it('takes over thoughts captured offline by older versions', () => {
localStorage.setItem(
'mindsift_offline_capture_queue',
JSON.stringify([{ id: 'th_offline_1', content: 'Alt', priority: 'normal', source: 'manual', createdAt: '2026-10-29T08:00:00Z' }])
);
const box = getOutbox();
expect(box).toHaveLength(1);
expect(box[0].path).toBe('/api/v1/inbox');
expect(localStorage.getItem('mindsift_offline_capture_queue')).toBeNull();
});
it('sends in order, drops rejected entries and stops while offline', async () => {
enqueue('POST', '/api/v1/inbox', { content: 'a' });
enqueue('PATCH', '/api/v1/lists/l/items/i', { isCompleted: true });
enqueue('POST', '/api/v1/inbox', { content: 'c' });
const answers = [res(201), res(404)];
const synced = await flushOutbox(async () => {
const next = answers.shift();
if (!next) throw new TypeError('offline');
return next;
});
expect(synced).toBe(1);
expect(getOutbox().map((e) => e.body.content)).toEqual(['c']);
});
it('does not cache streams, note sync or downloads', () => {
expect(isCacheablePath('/api/v1/thoughts?status=all')).toBe(true);
expect(isCacheablePath('/api/v1/events')).toBe(false);
expect(isCacheablePath('/api/v1/docs/d1/changes?since=3')).toBe(false);
expect(isCacheablePath('/api/v1/export/markdown')).toBe(false);
});
});