// ABOUTME: Tests for MMKV-based offline cache functionality
// ABOUTME: Validates query cache persistence, invalidation, and stale-while-revalidate behavior
import type { PersistedClient } from '@tanstack/react-query-persist-client';
import {
clearQueryCache,
clearAllStorage,
mmkvPersister,
queryKeys,
CACHE_TIMES,
} from '../src/utils/mmkvStorage';
// Helper to create a valid PersistedClient mock
function createMockPersistedClient(
data?: Record<string, unknown>
): PersistedClient {
return {
timestamp: Date.now(),
buster: 'v1',
clientState: {
queries: data
? [
{
queryKey: ['test'],
queryHash: 'test-hash',
state: {
data,
dataUpdateCount: 1,
dataUpdatedAt: Date.now(),
error: null,
errorUpdateCount: 0,
errorUpdatedAt: 0,
fetchFailureCount: 0,
fetchFailureReason: null,
fetchMeta: null,
fetchStatus: 'idle',
isInvalidated: false,
status: 'success',
},
},
]
: [],
mutations: [],
},
};
}
describe('mmkvStorage', () => {
beforeEach(() => {
// Clear storage before each test
clearAllStorage();
});
describe('mmkvPersister', () => {
it('should persist and restore client state', async () => {
const mockClient = createMockPersistedClient({ value: 'test-data' });
// Persist the client
await mmkvPersister.persistClient(mockClient);
// Restore the client
const restored = await mmkvPersister.restoreClient();
// Compare clientState since timestamps may differ slightly
expect(restored?.clientState).toEqual(mockClient.clientState);
expect(restored?.buster).toEqual(mockClient.buster);
});
it('should return undefined when no cache exists', async () => {
const restored = await mmkvPersister.restoreClient();
expect(restored).toBeUndefined();
});
it('should handle corrupted cache gracefully', async () => {
// This test would need to directly manipulate MMKV storage
// For now, we verify the restore returns undefined on error
const restored = await mmkvPersister.restoreClient();
expect(restored).toBeUndefined();
});
it('should remove client state', async () => {
const mockClient = createMockPersistedClient({ value: 'test' });
await mmkvPersister.persistClient(mockClient);
await mmkvPersister.removeClient();
const restored = await mmkvPersister.restoreClient();
expect(restored).toBeUndefined();
});
});
describe('clearQueryCache', () => {
it('should clear persisted query cache', async () => {
// Persist some data
const mockClient = createMockPersistedClient({ value: 'test' });
await mmkvPersister.persistClient(mockClient);
// Clear the cache
clearQueryCache();
// Verify it's gone
const restored = await mmkvPersister.restoreClient();
expect(restored).toBeUndefined();
});
});
describe('queryKeys', () => {
it('should generate consistent activity keys', () => {
const allKey = queryKeys.activities.all;
expect(allKey).toEqual(['activities']);
const listKey = queryKeys.activities.list({ days: 7, limit: 50 });
expect(listKey).toEqual(['activities', 'list', { days: 7, limit: 50 }]);
const detailKey = queryKeys.activities.detail('activity-123');
expect(detailKey).toEqual(['activities', 'detail', 'activity-123']);
});
it('should generate consistent training load keys', () => {
const allKey = queryKeys.trainingLoad.all;
expect(allKey).toEqual(['training-load']);
const currentKey = queryKeys.trainingLoad.current();
expect(currentKey).toEqual(['training-load', 'current']);
const historyKey = queryKeys.trainingLoad.history(30);
expect(historyKey).toEqual(['training-load', 'history', 30]);
});
it('should generate consistent recovery keys', () => {
const allKey = queryKeys.recovery.all;
expect(allKey).toEqual(['recovery']);
const currentKey = queryKeys.recovery.current();
expect(currentKey).toEqual(['recovery', 'current']);
const scoresKey = queryKeys.recovery.scores(7);
expect(scoresKey).toEqual(['recovery', 'scores', 7]);
});
});
describe('CACHE_TIMES', () => {
it('should have 7-day garbage collection time for activities', () => {
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
expect(CACHE_TIMES.ACTIVITIES_GC_TIME).toBe(sevenDaysMs);
});
it('should have 5-minute stale time for activities', () => {
const fiveMinutesMs = 5 * 60 * 1000;
expect(CACHE_TIMES.ACTIVITIES_STALE_TIME).toBe(fiveMinutesMs);
});
it('should have 15-minute stale time for training load', () => {
const fifteenMinutesMs = 15 * 60 * 1000;
expect(CACHE_TIMES.TRAINING_LOAD_STALE_TIME).toBe(fifteenMinutesMs);
});
it('should have 30-minute stale time for recovery', () => {
const thirtyMinutesMs = 30 * 60 * 1000;
expect(CACHE_TIMES.RECOVERY_STALE_TIME).toBe(thirtyMinutesMs);
});
it('should have 7-day max cache age', () => {
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
expect(CACHE_TIMES.MAX_CACHE_AGE).toBe(sevenDaysMs);
});
});
});