Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions src/__tests__/hooks/usePendingProofs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { usePendingProofs } from '../../hooks/usePendingProofs';
import { proofQueue } from '../../hooks/proofQueue';

// Mock proofQueue
jest.mock('../../hooks/proofQueue', () => ({
proofQueue: {
getPendingCount: jest.fn(),
syncPendingProofs: jest.fn(),
},
}));

describe('usePendingProofs', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should initialize with pending count', () => {
(proofQueue.getPendingCount as jest.Mock).mockReturnValue(3);

const { result } = renderHook(() => usePendingProofs());

expect(result.current.pendingCount).toBe(3);
expect(result.current.isSyncing).toBe(false);
expect(result.current.syncError).toBeNull();
});

it('should sync pending proofs', async () => {
const mockSyncResult = { successful: 2, failed: 0 };
(proofQueue.syncPendingProofs as jest.Mock).mockResolvedValue(mockSyncResult);

Check failure on line 30 in src/__tests__/hooks/usePendingProofs.test.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `mockSyncResult` with `⏎······mockSyncResult,⏎····`
(proofQueue.getPendingCount as jest.Mock).mockReturnValue(0);

const { result } = renderHook(() => usePendingProofs());

await act(async () => {
const syncResult = await result.current.syncPendingProofs();
expect(syncResult).toEqual(mockSyncResult);
});

expect(result.current.isSyncing).toBe(false);
expect(result.current.pendingCount).toBe(0);
});

it('should handle sync errors', async () => {
const error = new Error('Sync failed');
(proofQueue.syncPendingProofs as jest.Mock).mockRejectedValue(error);

const { result } = renderHook(() => usePendingProofs());

await act(async () => {
await expect(result.current.syncPendingProofs()).rejects.toThrow('Sync failed');

Check failure on line 51 in src/__tests__/hooks/usePendingProofs.test.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `'Sync·failed'` with `⏎········'Sync·failed',⏎······`
});

expect(result.current.isSyncing).toBe(false);
expect(result.current.syncError).toBe('Sync failed');
});

it('should not allow concurrent syncs', async () => {
(proofQueue.syncPendingProofs as jest.Mock).mockImplementation(
() => new Promise((resolve) => setTimeout(resolve, 100))

Check failure on line 60 in src/__tests__/hooks/usePendingProofs.test.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `(resolve)·=>·setTimeout(resolve,·100))` with `resolve·=>·setTimeout(resolve,·100)),`
);

const { result } = renderHook(() => usePendingProofs());

// Start first sync
let firstSyncPromise: Promise<any>;

Check failure on line 66 in src/__tests__/hooks/usePendingProofs.test.ts

View workflow job for this annotation

GitHub Actions / lint

'firstSyncPromise' is assigned a value but never used
await act(async () => {
firstSyncPromise = result.current.syncPendingProofs();
});

// Try to start second sync while first is in progress
let secondSyncResult: any;
await act(async () => {
secondSyncResult = result.current.syncPendingProofs();
});

expect(secondSyncResult).toBeUndefined();
expect(result.current.isSyncing).toBe(true);
});
});
115 changes: 115 additions & 0 deletions src/__tests__/hooks/useProofSubmit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { useProofSubmit } from '../../hooks/useProofSubmit';
import { proofQueue } from '../../hooks/proofQueue';

// Mock proofQueue
jest.mock('../../hooks/proofQueue', () => ({
proofQueue: {
getPendingCount: jest.fn(),
addProof: jest.fn(),
syncPendingProofs: jest.fn(),
},
}));

describe('useProofSubmit', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should initialize with default state', () => {
(proofQueue.getPendingCount as jest.Mock).mockReturnValue(0);

const { result } = renderHook(() => useProofSubmit());

expect(result.current.isSubmitting).toBe(false);
expect(result.current.progress).toEqual({ current: 0, total: 0 });
expect(result.current.error).toBeNull();
expect(result.current.pendingCount).toBe(0);
});

it('should submit a proof successfully', async () => {
const mockProof = { proof: 'test proof' };
const mockAddedProof = { id: 'proof_123', ...mockProof, timestamp: Date.now() };

Check failure on line 32 in src/__tests__/hooks/useProofSubmit.test.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `·id:·'proof_123',·...mockProof,·timestamp:·Date.now()` with `⏎······id:·'proof_123',⏎······...mockProof,⏎······timestamp:·Date.now(),⏎···`
const mockSyncResult = { successful: 1, failed: 0 };

(proofQueue.addProof as jest.Mock).mockReturnValue(mockAddedProof);
(proofQueue.syncPendingProofs as jest.Mock).mockResolvedValue(mockSyncResult);

Check failure on line 36 in src/__tests__/hooks/useProofSubmit.test.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `mockSyncResult` with `⏎······mockSyncResult,⏎····`
(proofQueue.getPendingCount as jest.Mock).mockReturnValue(0);

const { result } = renderHook(() => useProofSubmit());

let submitResult: any;
await act(async () => {
submitResult = await result.current.submit(mockProof);
});

expect(submitResult).toEqual({
success: true,
proofId: mockAddedProof.id,
syncResult: mockSyncResult,
});
expect(result.current.isSubmitting).toBe(false);
expect(result.current.error).toBeNull();
});

it('should handle submission errors', async () => {
const mockProof = { proof: 'test proof' };
const error = new Error('Submission failed');

(proofQueue.addProof as jest.Mock).mockImplementation(() => {
throw error;
});

const { result } = renderHook(() => useProofSubmit());

await act(async () => {
await expect(result.current.submit(mockProof)).rejects.toThrow('Submission failed');

Check failure on line 66 in src/__tests__/hooks/useProofSubmit.test.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `'Submission·failed'` with `⏎········'Submission·failed',⏎······`
});

expect(result.current.isSubmitting).toBe(false);
expect(result.current.error).toBe('Submission failed');
});

it('should handle sync errors during submission', async () => {
const mockProof = { proof: 'test proof' };
const mockAddedProof = { id: 'proof_123', ...mockProof, timestamp: Date.now() };

Check failure on line 75 in src/__tests__/hooks/useProofSubmit.test.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `·id:·'proof_123',·...mockProof,·timestamp:·Date.now()` with `⏎······id:·'proof_123',⏎······...mockProof,⏎······timestamp:·Date.now(),⏎···`
const error = new Error('Sync failed');

(proofQueue.addProof as jest.Mock).mockReturnValue(mockAddedProof);
(proofQueue.syncPendingProofs as jest.Mock).mockRejectedValue(error);

const { result } = renderHook(() => useProofSubmit());

await act(async () => {
await expect(result.current.submit(mockProof)).rejects.toThrow('Sync failed');

Check failure on line 84 in src/__tests__/hooks/useProofSubmit.test.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `'Sync·failed'` with `⏎········'Sync·failed',⏎······`
});

expect(result.current.isSubmitting).toBe(false);
expect(result.current.error).toBe('Sync failed');
});

it('should update progress during sync', async () => {
const mockProof = { proof: 'test proof' };
const mockAddedProof = { id: 'proof_123', ...mockProof, timestamp: Date.now() };

Check failure on line 93 in src/__tests__/hooks/useProofSubmit.test.ts

View workflow job for this annotation

GitHub Actions / lint

Replace `·id:·'proof_123',·...mockProof,·timestamp:·Date.now()` with `⏎······id:·'proof_123',⏎······...mockProof,⏎······timestamp:·Date.now(),⏎···`

(proofQueue.addProof as jest.Mock).mockReturnValue(mockAddedProof);
(proofQueue.syncPendingProofs as jest.Mock).mockImplementation(
async (onProgress?: (current: number, total: number) => void) => {
if (onProgress) {
onProgress(1, 2);
onProgress(2, 2);
}
return { successful: 2, failed: 0 };
}
);
(proofQueue.getPendingCount as jest.Mock).mockReturnValue(0);

const { result } = renderHook(() => useProofSubmit());

await act(async () => {
await result.current.submit(mockProof);
});

expect(result.current.progress).toEqual({ current: 0, total: 0 });
});
});
120 changes: 120 additions & 0 deletions src/hooks/proofQueue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { useState, useCallback } from 'react';

export interface ProofData {
id: string;
proof: string;
timestamp: number;
}

export interface ProofSubmitResult {
success: boolean;
proofId: string;
error?: string;
}

/**
* Shared proof queue module
* Used by both usePendingProofs and useProofSubmit
*/
export const proofQueue = {
/**
* Get all pending proofs
*/
getPendingProofs: (): ProofData[] => {
try {
const stored = localStorage.getItem('pendingProofs');
return stored ? JSON.parse(stored) : [];
} catch {
return [];
}
},

/**
* Add a proof to the queue
*/
addProof: (proof: Omit<ProofData, 'id' | 'timestamp'>): ProofData => {
const pending = proofQueue.getPendingProofs();
const newProof: ProofData = {
...proof,
id: `proof_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
timestamp: Date.now(),
};
pending.push(newProof);
localStorage.setItem('pendingProofs', JSON.stringify(pending));
return newProof;
},

/**
* Remove a proof from the queue
*/
removeProof: (proofId: string): void => {
const pending = proofQueue.getPendingProofs();
const filtered = pending.filter((p) => p.id !== proofId);
localStorage.setItem('pendingProofs', JSON.stringify(filtered));
},

/**
* Clear all pending proofs
*/
clearAll: (): void => {
localStorage.removeItem('pendingProofs');
},

/**
* Get the count of pending proofs
*/
getPendingCount: (): number => {
return proofQueue.getPendingProofs().length;
},

/**
* Submit a single proof
*/
submitProof: async (proof: ProofData): Promise<ProofSubmitResult> => {
// Simulate API call
await new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() < 0.1) {
reject(new Error('Network error'));
} else {
resolve({});
}
}, 1000);
});

return {
success: true,
proofId: proof.id,
};
},

/**
* Sync all pending proofs
*/
syncPendingProofs: async (onProgress?: (completed: number, total: number) => void): Promise<{
successful: number;
failed: number;
}> => {
const pending = proofQueue.getPendingProofs();
let successful = 0;
let failed = 0;

for (let i = 0; i < pending.length; i++) {
const proof = pending[i];
try {
await proofQueue.submitProof(proof);
proofQueue.removeProof(proof.id);
successful++;
} catch (error) {
failed++;
console.error(`Failed to submit proof ${proof.id}:`, error);
}

if (onProgress) {
onProgress(i + 1, pending.length);
}
}

return { successful, failed };
},
};
79 changes: 79 additions & 0 deletions src/hooks/usePendingProofs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useState, useEffect, useCallback } from 'react';
import { proofQueue } from './proofQueue';

/**
* Hook for managing pending proofs (used by HomeScreen)
* Only manages the queue count and sync - no submission state
*
* @returns {Object} pendingCount, isSyncing, syncPendingProofs
*/
export function usePendingProofs() {
const [pendingCount, setPendingCount] = useState(0);
const [isSyncing, setIsSyncing] = useState(false);
const [syncError, setSyncError] = useState<string | null>(null);

// Update pending count on mount and when storage changes
useEffect(() => {
const updateCount = () => {
const count = proofQueue.getPendingCount();
setPendingCount(count);
};

updateCount();

// Listen for storage changes (e.g., from other tabs)
const handleStorageChange = (e: StorageEvent) => {
if (e.key === 'pendingProofs') {
updateCount();
}
};

// Custom event for same-tab updates
const handleProofUpdate = () => {
updateCount();
};

window.addEventListener('storage', handleStorageChange);
window.addEventListener('proofsUpdated', handleProofUpdate);

return () => {
window.removeEventListener('storage', handleStorageChange);
window.removeEventListener('proofsUpdated', handleProofUpdate);
};
}, []);

/**
* Sync all pending proofs
*/
const syncPendingProofs = useCallback(async () => {
if (isSyncing) return;

setIsSyncing(true);
setSyncError(null);

try {
const result = await proofQueue.syncPendingProofs();

// Update count after sync
const newCount = proofQueue.getPendingCount();
setPendingCount(newCount);

// Dispatch event to update other components
window.dispatchEvent(new Event('proofsUpdated'));

return result;
} catch (error) {
setSyncError(error instanceof Error ? error.message : 'Sync failed');
throw error;
} finally {
setIsSyncing(false);
}
}, [isSyncing]);

return {
pendingCount,
isSyncing,
syncPendingProofs,
syncError,
};
}
Loading
Loading