mirror of
https://github.com/seemueller-io/yachtpit.git
synced 2025-09-08 22:46:45 +00:00

- Prevent AIS stream from starting immediately; start upon user interaction. - Add `ais_stream_started` state for WebSocket management. - Extend `useRealAISProvider` with `userLocationLoaded` and `mapFocused` to control stream. - Update frontend components to handle geolocation and map focus. - Exclude test files from compilation Introduce WebSocket integration for AIS services - Added WebSocket-based `useRealAISProvider` React hook for real-time AIS vessel data. - Created various tests including unit, integration, and browser tests to validate WebSocket functionality. - Added `ws` dependency to enable WebSocket communication. - Implemented vessel data mapping and bounding box handling for dynamic updates.
246 lines
7.4 KiB
TypeScript
246 lines
7.4 KiB
TypeScript
import { renderHook, act } from '@testing-library/react';
|
|
import { vi } from 'vitest';
|
|
import { useRealAISProvider } from '../src/real-ais-provider.tsx';
|
|
|
|
// Mock WebSocket
|
|
class MockWebSocket {
|
|
static instances: MockWebSocket[] = [];
|
|
static CONNECTING = 0;
|
|
static OPEN = 1;
|
|
static CLOSING = 2;
|
|
static CLOSED = 3;
|
|
|
|
readyState: number = MockWebSocket.CONNECTING;
|
|
onopen: ((event: Event) => void) | null = null;
|
|
onmessage: ((event: MessageEvent) => void) | null = null;
|
|
onerror: ((event: Event) => void) | null = null;
|
|
onclose: ((event: CloseEvent) => void) | null = null;
|
|
|
|
constructor(public url: string) {
|
|
MockWebSocket.instances.push(this);
|
|
|
|
// Simulate connection opening after a short delay
|
|
setTimeout(() => {
|
|
this.readyState = MockWebSocket.OPEN;
|
|
if (this.onopen) {
|
|
this.onopen(new Event('open'));
|
|
}
|
|
}, 10);
|
|
}
|
|
|
|
send(data: string) {
|
|
console.log('MockWebSocket send:', data);
|
|
}
|
|
|
|
close() {
|
|
this.readyState = MockWebSocket.CLOSED;
|
|
if (this.onclose) {
|
|
this.onclose(new CloseEvent('close', { wasClean: true }));
|
|
}
|
|
}
|
|
|
|
static reset() {
|
|
MockWebSocket.instances = [];
|
|
}
|
|
|
|
static getConnectionCount() {
|
|
return MockWebSocket.instances.filter(ws =>
|
|
ws.readyState === MockWebSocket.OPEN ||
|
|
ws.readyState === MockWebSocket.CONNECTING
|
|
).length;
|
|
}
|
|
}
|
|
|
|
// Replace global WebSocket with mock
|
|
(global as any).WebSocket = MockWebSocket;
|
|
|
|
describe('useRealAISProvider WebSocket Connection Management', () => {
|
|
beforeEach(() => {
|
|
MockWebSocket.reset();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
MockWebSocket.reset();
|
|
});
|
|
|
|
test('should create only one WebSocket connection on initial render', async () => {
|
|
const boundingBox = {
|
|
sw_lat: 33.0,
|
|
sw_lon: -119.0,
|
|
ne_lat: 34.0,
|
|
ne_lon: -118.0
|
|
};
|
|
|
|
const { result } = renderHook(() => useRealAISProvider(boundingBox));
|
|
|
|
// Wait for connection to be established
|
|
await act(async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
});
|
|
|
|
expect(MockWebSocket.instances).toHaveLength(1);
|
|
expect(MockWebSocket.getConnectionCount()).toBe(1);
|
|
expect(result.current.isConnected).toBe(true);
|
|
});
|
|
|
|
test('should not create multiple connections when bounding box changes', async () => {
|
|
const initialBoundingBox = {
|
|
sw_lat: 33.0,
|
|
sw_lon: -119.0,
|
|
ne_lat: 34.0,
|
|
ne_lon: -118.0
|
|
};
|
|
|
|
const { result, rerender } = renderHook(
|
|
({ boundingBox }) => useRealAISProvider(boundingBox),
|
|
{ initialProps: { boundingBox: initialBoundingBox } }
|
|
);
|
|
|
|
// Wait for initial connection
|
|
await act(async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
});
|
|
|
|
expect(MockWebSocket.instances).toHaveLength(1);
|
|
expect(MockWebSocket.getConnectionCount()).toBe(1);
|
|
|
|
// Change bounding box multiple times
|
|
const newBoundingBox1 = {
|
|
sw_lat: 34.0,
|
|
sw_lon: -120.0,
|
|
ne_lat: 35.0,
|
|
ne_lon: -119.0
|
|
};
|
|
|
|
const newBoundingBox2 = {
|
|
sw_lat: 35.0,
|
|
sw_lon: -121.0,
|
|
ne_lat: 36.0,
|
|
ne_lon: -120.0
|
|
};
|
|
|
|
await act(async () => {
|
|
rerender({ boundingBox: newBoundingBox1 });
|
|
await new Promise(resolve => setTimeout(resolve, 20));
|
|
});
|
|
|
|
await act(async () => {
|
|
rerender({ boundingBox: newBoundingBox2 });
|
|
await new Promise(resolve => setTimeout(resolve, 20));
|
|
});
|
|
|
|
// Should still have only one connection
|
|
expect(MockWebSocket.getConnectionCount()).toBe(1);
|
|
expect(result.current.isConnected).toBe(true);
|
|
});
|
|
|
|
test('should properly cleanup connection when component unmounts', async () => {
|
|
const boundingBox = {
|
|
sw_lat: 33.0,
|
|
sw_lon: -119.0,
|
|
ne_lat: 34.0,
|
|
ne_lon: -118.0
|
|
};
|
|
|
|
const { result, unmount } = renderHook(() => useRealAISProvider(boundingBox));
|
|
|
|
// Wait for connection
|
|
await act(async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
});
|
|
|
|
expect(MockWebSocket.getConnectionCount()).toBe(1);
|
|
expect(result.current.isConnected).toBe(true);
|
|
|
|
// Unmount component
|
|
unmount();
|
|
|
|
// Connection should be closed
|
|
expect(MockWebSocket.instances[0].readyState).toBe(MockWebSocket.CLOSED);
|
|
});
|
|
|
|
test('should not create connection when isActive is false', async () => {
|
|
const boundingBox = {
|
|
sw_lat: 33.0,
|
|
sw_lon: -119.0,
|
|
ne_lat: 34.0,
|
|
ne_lon: -118.0
|
|
};
|
|
|
|
// Create a custom hook that starts with isActive = false
|
|
const { result } = renderHook(() => {
|
|
const provider = useRealAISProvider(boundingBox);
|
|
// Set inactive immediately on first render
|
|
if (provider.isActive) {
|
|
provider.setIsActive(false);
|
|
}
|
|
return provider;
|
|
});
|
|
|
|
// Wait a bit to ensure no connection is created
|
|
await act(async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
});
|
|
|
|
expect(MockWebSocket.instances).toHaveLength(0);
|
|
expect(result.current.isConnected).toBe(false);
|
|
expect(result.current.isActive).toBe(false);
|
|
});
|
|
|
|
test('should handle reconnection properly without creating multiple connections', async () => {
|
|
const boundingBox = {
|
|
sw_lat: 33.0,
|
|
sw_lon: -119.0,
|
|
ne_lat: 34.0,
|
|
ne_lon: -118.0
|
|
};
|
|
|
|
const { result } = renderHook(() => useRealAISProvider(boundingBox));
|
|
|
|
// Wait for initial connection
|
|
await act(async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
});
|
|
|
|
expect(MockWebSocket.getConnectionCount()).toBe(1);
|
|
|
|
// Simulate connection loss
|
|
await act(async () => {
|
|
const ws = MockWebSocket.instances[0];
|
|
ws.readyState = MockWebSocket.CLOSED;
|
|
if (ws.onclose) {
|
|
ws.onclose(new CloseEvent('close', { wasClean: false }));
|
|
}
|
|
await new Promise(resolve => setTimeout(resolve, 3100)); // Wait for reconnection timeout
|
|
});
|
|
|
|
// Should have attempted reconnection but still only one active connection
|
|
expect(MockWebSocket.getConnectionCount()).toBe(1);
|
|
});
|
|
|
|
test('should send bounding box configuration on connection', async () => {
|
|
const boundingBox = {
|
|
sw_lat: 33.0,
|
|
sw_lon: -119.0,
|
|
ne_lat: 34.0,
|
|
ne_lon: -118.0
|
|
};
|
|
|
|
const sendSpy = vi.spyOn(MockWebSocket.prototype, 'send');
|
|
|
|
const { result } = renderHook(() => useRealAISProvider(boundingBox));
|
|
|
|
// Wait for connection and bounding box message
|
|
await act(async () => {
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
});
|
|
|
|
expect(sendSpy).toHaveBeenCalledWith(
|
|
JSON.stringify({
|
|
type: 'set_bounding_box',
|
|
bounding_box: boundingBox
|
|
})
|
|
);
|
|
});
|
|
}); |