108 lines
3 KiB
TypeScript
108 lines
3 KiB
TypeScript
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||
|
|
import { renderHook, act } from '@testing-library/react';
|
||
|
|
import { useShoppingListSync } from './useShoppingListSync';
|
||
|
|
|
||
|
|
vi.mock('@/services/shopping-lists', () => ({
|
||
|
|
getShoppingListSyncSocketUrl: vi.fn(() => 'ws://localhost/sync'),
|
||
|
|
}));
|
||
|
|
|
||
|
|
class MockWebSocket {
|
||
|
|
static OPEN = 1;
|
||
|
|
static CONNECTING = 0;
|
||
|
|
url: string;
|
||
|
|
readyState = 1; // OPEN
|
||
|
|
onopen: any = null;
|
||
|
|
onmessage: any = null;
|
||
|
|
onerror: any = null;
|
||
|
|
onclose: any = null;
|
||
|
|
send = vi.fn();
|
||
|
|
close = vi.fn();
|
||
|
|
|
||
|
|
constructor(url: string) {
|
||
|
|
this.url = url;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('useShoppingListSync', () => {
|
||
|
|
let originalWebSocket: any;
|
||
|
|
let createdSockets: MockWebSocket[] = [];
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
vi.clearAllMocks();
|
||
|
|
createdSockets = [];
|
||
|
|
originalWebSocket = global.WebSocket;
|
||
|
|
const MockClass = class extends MockWebSocket {
|
||
|
|
constructor(url: string) {
|
||
|
|
super(url);
|
||
|
|
createdSockets.push(this);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
(MockClass as any).OPEN = 1;
|
||
|
|
(MockClass as any).CONNECTING = 0;
|
||
|
|
global.WebSocket = MockClass as any;
|
||
|
|
});
|
||
|
|
|
||
|
|
afterEach(() => {
|
||
|
|
global.WebSocket = originalWebSocket;
|
||
|
|
});
|
||
|
|
|
||
|
|
it('initializes and connects to the correct socket URL', () => {
|
||
|
|
const onSync = vi.fn();
|
||
|
|
renderHook(() => useShoppingListSync('hh1', 'list1', onSync));
|
||
|
|
|
||
|
|
expect(createdSockets).toHaveLength(1);
|
||
|
|
expect(createdSockets[0].url).toBe('ws://localhost/sync');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles incoming item_updated messages correctly', () => {
|
||
|
|
const onSync = vi.fn();
|
||
|
|
renderHook(() => useShoppingListSync('hh1', 'list1', onSync));
|
||
|
|
|
||
|
|
const ws = createdSockets[0];
|
||
|
|
act(() => { if (ws.onopen) ws.onopen(); });
|
||
|
|
|
||
|
|
act(() => {
|
||
|
|
if (ws.onmessage) ws.onmessage({ data: JSON.stringify({ type: 'ITEM_UPDATED', itemId: 'item1', updates: { checked: true } }) });
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(onSync).toHaveBeenCalledWith({ type: 'ITEM_UPDATED', itemId: 'item1', updates: { checked: true } });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('broadcasts toggle item messages when connected', () => {
|
||
|
|
const onSync = vi.fn();
|
||
|
|
const { result } = renderHook(() => useShoppingListSync('hh1', 'list1', onSync));
|
||
|
|
|
||
|
|
const ws = createdSockets[0];
|
||
|
|
act(() => { if (ws.onopen) ws.onopen(); });
|
||
|
|
|
||
|
|
act(() => {
|
||
|
|
result.current.toggleItemCheck('item1', true);
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(ws.send).toHaveBeenCalledWith(JSON.stringify({
|
||
|
|
type: 'TOGGLE_ITEM',
|
||
|
|
itemId: 'item1',
|
||
|
|
checked: true,
|
||
|
|
}));
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles disconnect and reconnect backoff', () => {
|
||
|
|
vi.useFakeTimers();
|
||
|
|
const onSync = vi.fn();
|
||
|
|
const { result } = renderHook(() => useShoppingListSync('hh1', 'list1', onSync));
|
||
|
|
|
||
|
|
let ws = createdSockets[0];
|
||
|
|
act(() => { if (ws.onopen) ws.onopen(); });
|
||
|
|
expect(result.current.isConnected).toBe(true);
|
||
|
|
|
||
|
|
act(() => { if (ws.onclose) ws.onclose({ reason: 'test' }); });
|
||
|
|
expect(result.current.isConnected).toBe(false);
|
||
|
|
|
||
|
|
// After 1000ms it should attempt reconnect
|
||
|
|
act(() => { vi.advanceTimersByTime(1000); });
|
||
|
|
expect(createdSockets).toHaveLength(2);
|
||
|
|
|
||
|
|
vi.useRealTimers();
|
||
|
|
});
|
||
|
|
});
|