mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-24 00:51:53 +00:00
147 lines
4.3 KiB
TypeScript
147 lines
4.3 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { connectHeartbeat } from '../../src/services/backend-client';
|
|
|
|
// Mock EventSource to simulate SSE behavior
|
|
class MockEventSource {
|
|
onopen: (() => void) | null = null;
|
|
onerror: (() => void) | null = null;
|
|
closed = false;
|
|
|
|
close() {
|
|
this.closed = true;
|
|
}
|
|
}
|
|
|
|
let lastEventSource: MockEventSource | null = null;
|
|
|
|
beforeEach(() => {
|
|
lastEventSource = null;
|
|
vi.stubGlobal(
|
|
'EventSource',
|
|
vi.fn().mockImplementation(() => {
|
|
lastEventSource = new MockEventSource();
|
|
return lastEventSource;
|
|
}),
|
|
);
|
|
vi.useFakeTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
describe('connectHeartbeat', () => {
|
|
it('calls onConnect when EventSource opens', () => {
|
|
const onConnect = vi.fn();
|
|
const onReconnecting = vi.fn();
|
|
connectHeartbeat(onConnect, onReconnecting);
|
|
|
|
lastEventSource!.onopen!();
|
|
expect(onConnect).toHaveBeenCalledOnce();
|
|
expect(onReconnecting).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('calls onReconnecting on first error, then retries', () => {
|
|
const onConnect = vi.fn();
|
|
const onReconnecting = vi.fn();
|
|
connectHeartbeat(onConnect, onReconnecting);
|
|
|
|
// Simulate connection drop
|
|
lastEventSource!.onerror!();
|
|
|
|
expect(onReconnecting).toHaveBeenCalledOnce();
|
|
expect(lastEventSource!.closed).toBe(true);
|
|
|
|
// Advance past first retry delay (1s)
|
|
vi.advanceTimersByTime(1_000);
|
|
|
|
// A new EventSource should have been created
|
|
expect(EventSource).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('fires onReconnecting only once per disconnect', () => {
|
|
const onConnect = vi.fn();
|
|
const onReconnecting = vi.fn();
|
|
connectHeartbeat(onConnect, onReconnecting);
|
|
|
|
// First error
|
|
lastEventSource!.onerror!();
|
|
expect(onReconnecting).toHaveBeenCalledOnce();
|
|
|
|
// Second retry fires error again
|
|
vi.advanceTimersByTime(1_000);
|
|
lastEventSource!.onerror!();
|
|
expect(onReconnecting).toHaveBeenCalledOnce(); // still 1
|
|
|
|
// Third retry fires error
|
|
vi.advanceTimersByTime(2_000);
|
|
lastEventSource!.onerror!();
|
|
expect(onReconnecting).toHaveBeenCalledOnce(); // still 1
|
|
});
|
|
|
|
it('retries indefinitely instead of giving up after 3 attempts', () => {
|
|
const onConnect = vi.fn();
|
|
const onReconnecting = vi.fn();
|
|
connectHeartbeat(onConnect, onReconnecting);
|
|
|
|
// Simulate 10 consecutive failures — should never stop retrying
|
|
for (let i = 0; i < 10; i++) {
|
|
lastEventSource!.onerror!();
|
|
// Advance past the max backoff (15s) to ensure the next retry fires
|
|
vi.advanceTimersByTime(16_000);
|
|
}
|
|
|
|
// Should have created 11 EventSources (1 initial + 10 retries)
|
|
expect(EventSource).toHaveBeenCalledTimes(11);
|
|
});
|
|
|
|
it('resets reconnecting state when connection recovers', () => {
|
|
const onConnect = vi.fn();
|
|
const onReconnecting = vi.fn();
|
|
connectHeartbeat(onConnect, onReconnecting);
|
|
|
|
// Drop
|
|
lastEventSource!.onerror!();
|
|
expect(onReconnecting).toHaveBeenCalledOnce();
|
|
|
|
// Retry succeeds
|
|
vi.advanceTimersByTime(1_000);
|
|
lastEventSource!.onopen!();
|
|
expect(onConnect).toHaveBeenCalledOnce();
|
|
|
|
// Drop again — should fire onReconnecting again (reset after recovery)
|
|
lastEventSource!.onerror!();
|
|
expect(onReconnecting).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('caps backoff at 15 seconds', () => {
|
|
const onConnect = vi.fn();
|
|
const onReconnecting = vi.fn();
|
|
connectHeartbeat(onConnect, onReconnecting);
|
|
|
|
// Fail many times to push backoff past the cap
|
|
for (let i = 0; i < 6; i++) {
|
|
lastEventSource!.onerror!();
|
|
// The delay for attempt i is min(1000 * 2^i, 15000)
|
|
// i=0: 1s, i=1: 2s, i=2: 4s, i=3: 8s, i=4: 15s (capped), i=5: 15s (capped)
|
|
vi.advanceTimersByTime(16_000);
|
|
}
|
|
|
|
// All retries should have fired — 7 EventSources total
|
|
expect(EventSource).toHaveBeenCalledTimes(7);
|
|
});
|
|
|
|
it('stops retrying when cleanup is called', () => {
|
|
const onConnect = vi.fn();
|
|
const onReconnecting = vi.fn();
|
|
const cleanup = connectHeartbeat(onConnect, onReconnecting);
|
|
|
|
lastEventSource!.onerror!();
|
|
cleanup();
|
|
|
|
// Advance time — no new EventSource should be created
|
|
vi.advanceTimersByTime(30_000);
|
|
expect(EventSource).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|