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>
50 lines
2.0 KiB
TypeScript
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);
|
|
});
|
|
});
|