Testing Guide
How to test code that uses Connectivity.
Test isolation
ConnectivityClient is a singleton. Reset it after each test to prevent state leakage:
import { describe, test, afterEach, vi } from 'vitest';
import { ConnectivityClient } from '@connectivity-js/core';
afterEach(() => {
ConnectivityClient.resetInstance();
vi.restoreAllMocks();
});Mock Detector
Control network state programmatically in tests:
import type { Detector, DetectorEvent } from '@connectivity-js/core';
const createMockDetector = () => {
let listener: ((event: DetectorEvent) => void) | null = null;
const detector: Detector = {
start: (cb) => {
listener = cb;
return () => { listener = null; };
},
};
const emit = (event: DetectorEvent) => listener?.(event);
const goOnline = () => emit({ status: 'online', reason: 'mock' });
const goOffline = () => emit({ status: 'offline', reason: 'mock' });
return { detector, emit, goOnline, goOffline };
};Basic test patterns
import { getConnectivityClient, ConnectivityClient } from '@connectivity-js/core';
describe('My feature', () => {
afterEach(() => {
ConnectivityClient.resetInstance();
});
test('saves immediately when online', async () => {
const { detector, goOnline } = createMockDetector();
const client = getConnectivityClient({
detectors: [detector],
initialStatus: 'online',
});
client.start();
goOnline();
const mockRequest = vi.fn().mockResolvedValue({ success: true });
client.registerAction('save', {
request: mockRequest,
options: { whenOffline: 'queue' },
});
const result = await client.execute('save', { id: '1', data: 'hello' });
expect(result.enqueued).toBe(false);
expect(mockRequest).toHaveBeenCalledWith({ id: '1', data: 'hello' });
});
test('queues when offline', async () => {
const { detector, goOffline } = createMockDetector();
const client = getConnectivityClient({
detectors: [detector],
initialStatus: 'offline',
});
client.start();
goOffline();
client.registerAction('save', {
request: vi.fn(),
options: { whenOffline: 'queue' },
});
const result = await client.execute('save', { id: '1', data: 'hello' });
expect(result.enqueued).toBe(true);
});
});React hook testing
Use @testing-library/react’s renderHook:
import { renderHook, act } from '@testing-library/react';
import { useConnectivity } from '@connectivity-js/react';
import { getConnectivityClient, ConnectivityClient } from '@connectivity-js/core';
describe('useConnectivity', () => {
afterEach(() => {
ConnectivityClient.resetInstance();
});
test('subscribes to state changes', () => {
const { detector, goOnline, goOffline } = createMockDetector();
getConnectivityClient({ detectors: [detector], initialStatus: 'unknown' });
getConnectivityClient().start();
const { result } = renderHook(() => useConnectivity());
act(() => goOnline());
expect(result.current.status).toBe('online');
act(() => goOffline());
expect(result.current.status).toBe('offline');
});
});Timer testing
For retry, grace period, and other timer-based features, use vi.useFakeTimers():
test('retry executes after backoff', async () => {
vi.useFakeTimers();
const { detector, goOnline } = createMockDetector();
const client = getConnectivityClient({ detectors: [detector], initialStatus: 'online' });
client.start();
goOnline();
const mockRequest = vi.fn()
.mockRejectedValueOnce(new Error('fail'))
.mockResolvedValueOnce({ ok: true });
client.registerAction('save', {
request: mockRequest,
options: {
retry: { maxAttempts: 2, backoffMs: () => 1_000 },
},
});
const resultPromise = client.execute('save', { data: 'test' });
await vi.advanceTimersByTimeAsync(1_000);
const result = await resultPromise;
expect(result.enqueued).toBe(true);
vi.useRealTimers();
});actionKey conventions
actionKey is a unique string identifying an action. To prevent collisions:
// ✅ Good — domain:verb pattern
'document:save'
'document:export'
'user:updateProfile'
'analytics:trackEvent'
// ✅ Good — slash separator
'document/save'
'document/export'
// ❌ Bad — too generic
'save'
'update'
'fetch'Recommended: centralize action keys:
// constants/action-keys.ts
export const ACTION_KEYS = {
DOCUMENT_SAVE: 'document:save',
DOCUMENT_EXPORT: 'document:export',
USER_UPDATE_PROFILE: 'user:updateProfile',
} as const;Related
Last updated on