Files
flyer-crawler.projectium.com/src/hooks/useEventBus.test.ts
Torben Sorensen 45ac4fccf5
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 2m15s
comprehensive documentation review + test fixes
2026-01-28 16:35:38 -08:00

312 lines
9.4 KiB
TypeScript

// src/hooks/useEventBus.test.ts
import { renderHook } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest';
import { useEventBus } from './useEventBus';
// Mock the eventBus service
vi.mock('../services/eventBus', () => ({
eventBus: {
on: vi.fn(),
off: vi.fn(),
dispatch: vi.fn(),
},
}));
import { eventBus } from '../services/eventBus';
const mockEventBus = eventBus as unknown as {
on: Mock;
off: Mock;
dispatch: Mock;
};
describe('useEventBus', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('subscription', () => {
it('should subscribe to the event on mount', () => {
const callback = vi.fn();
renderHook(() => useEventBus('test-event', callback));
expect(mockEventBus.on).toHaveBeenCalledTimes(1);
expect(mockEventBus.on).toHaveBeenCalledWith('test-event', expect.any(Function));
});
it('should unsubscribe from the event on unmount', () => {
const callback = vi.fn();
const { unmount } = renderHook(() => useEventBus('test-event', callback));
unmount();
expect(mockEventBus.off).toHaveBeenCalledTimes(1);
expect(mockEventBus.off).toHaveBeenCalledWith('test-event', expect.any(Function));
});
it('should pass the same callback reference to on and off', () => {
const callback = vi.fn();
const { unmount } = renderHook(() => useEventBus('test-event', callback));
const onCallback = mockEventBus.on.mock.calls[0][1];
unmount();
const offCallback = mockEventBus.off.mock.calls[0][1];
expect(onCallback).toBe(offCallback);
});
});
describe('callback execution', () => {
it('should call the callback when event is dispatched', () => {
const callback = vi.fn();
renderHook(() => useEventBus('test-event', callback));
// Get the registered callback and call it
const registeredCallback = mockEventBus.on.mock.calls[0][1];
registeredCallback({ message: 'hello' });
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith({ message: 'hello' });
});
it('should call the callback with undefined data', () => {
const callback = vi.fn();
renderHook(() => useEventBus('test-event', callback));
const registeredCallback = mockEventBus.on.mock.calls[0][1];
registeredCallback(undefined);
expect(callback).toHaveBeenCalledWith(undefined);
});
it('should call the callback with null data', () => {
const callback = vi.fn();
renderHook(() => useEventBus('test-event', callback));
const registeredCallback = mockEventBus.on.mock.calls[0][1];
registeredCallback(null);
expect(callback).toHaveBeenCalledWith(null);
});
});
describe('callback ref updates', () => {
it('should use the latest callback when event is dispatched', () => {
const callback1 = vi.fn();
const callback2 = vi.fn();
const { rerender } = renderHook(({ callback }) => useEventBus('test-event', callback), {
initialProps: { callback: callback1 },
});
// Rerender with new callback
rerender({ callback: callback2 });
// Get the registered callback and call it
const registeredCallback = mockEventBus.on.mock.calls[0][1];
registeredCallback({ message: 'hello' });
// Should call the new callback, not the old one
expect(callback1).not.toHaveBeenCalled();
expect(callback2).toHaveBeenCalledTimes(1);
expect(callback2).toHaveBeenCalledWith({ message: 'hello' });
});
it('should not re-subscribe when callback changes', () => {
const callback1 = vi.fn();
const callback2 = vi.fn();
const { rerender } = renderHook(({ callback }) => useEventBus('test-event', callback), {
initialProps: { callback: callback1 },
});
// Clear mock counts
mockEventBus.on.mockClear();
mockEventBus.off.mockClear();
// Rerender with new callback
rerender({ callback: callback2 });
// Should NOT unsubscribe and re-subscribe
expect(mockEventBus.off).not.toHaveBeenCalled();
expect(mockEventBus.on).not.toHaveBeenCalled();
});
});
describe('event name changes', () => {
it('should re-subscribe when event name changes', () => {
const callback = vi.fn();
const { rerender } = renderHook(({ event }) => useEventBus(event, callback), {
initialProps: { event: 'event-1' },
});
// Initial subscription
expect(mockEventBus.on).toHaveBeenCalledTimes(1);
expect(mockEventBus.on).toHaveBeenCalledWith('event-1', expect.any(Function));
// Clear mock
mockEventBus.on.mockClear();
// Rerender with different event
rerender({ event: 'event-2' });
// Should unsubscribe from old event
expect(mockEventBus.off).toHaveBeenCalledWith('event-1', expect.any(Function));
// Should subscribe to new event
expect(mockEventBus.on).toHaveBeenCalledWith('event-2', expect.any(Function));
});
});
describe('multiple hooks', () => {
it('should allow multiple subscriptions to same event', () => {
const callback1 = vi.fn();
const callback2 = vi.fn();
renderHook(() => useEventBus('shared-event', callback1));
renderHook(() => useEventBus('shared-event', callback2));
expect(mockEventBus.on).toHaveBeenCalledTimes(2);
// Both should be subscribed to same event
expect(mockEventBus.on.mock.calls[0][0]).toBe('shared-event');
expect(mockEventBus.on.mock.calls[1][0]).toBe('shared-event');
});
it('should allow subscriptions to different events', () => {
const callback1 = vi.fn();
const callback2 = vi.fn();
renderHook(() => useEventBus('event-a', callback1));
renderHook(() => useEventBus('event-b', callback2));
expect(mockEventBus.on).toHaveBeenCalledTimes(2);
expect(mockEventBus.on).toHaveBeenCalledWith('event-a', expect.any(Function));
expect(mockEventBus.on).toHaveBeenCalledWith('event-b', expect.any(Function));
});
});
describe('type safety', () => {
it('should correctly type the callback data', () => {
interface TestData {
id: number;
name: string;
}
const callback = vi.fn();
renderHook(() => useEventBus<TestData>('typed-event', callback));
const registeredCallback = mockEventBus.on.mock.calls[0][1];
registeredCallback({ id: 1, name: 'test' });
expect(callback).toHaveBeenCalledWith({ id: 1, name: 'test' });
});
it('should handle callback with optional parameter', () => {
const callback = vi.fn();
renderHook(() => useEventBus<string>('optional-event', callback));
const registeredCallback = mockEventBus.on.mock.calls[0][1];
// Call with data
registeredCallback('hello');
expect(callback).toHaveBeenCalledWith('hello');
// Call without data
registeredCallback();
expect(callback).toHaveBeenCalledWith(undefined);
});
});
describe('edge cases', () => {
it('should handle empty string event name', () => {
const callback = vi.fn();
renderHook(() => useEventBus('', callback));
expect(mockEventBus.on).toHaveBeenCalledWith('', expect.any(Function));
});
it('should handle event names with special characters', () => {
const callback = vi.fn();
renderHook(() => useEventBus('event:with:colons', callback));
expect(mockEventBus.on).toHaveBeenCalledWith('event:with:colons', expect.any(Function));
});
it('should handle rapid mount/unmount cycles', () => {
const callback = vi.fn();
const { unmount: unmount1 } = renderHook(() => useEventBus('rapid-event', callback));
unmount1();
const { unmount: unmount2 } = renderHook(() => useEventBus('rapid-event', callback));
unmount2();
const { unmount: unmount3 } = renderHook(() => useEventBus('rapid-event', callback));
unmount3();
// Should have 3 subscriptions and 3 unsubscriptions
expect(mockEventBus.on).toHaveBeenCalledTimes(3);
expect(mockEventBus.off).toHaveBeenCalledTimes(3);
});
});
describe('stable callback reference', () => {
it('should use useCallback for stable reference', () => {
const callback = vi.fn();
const { rerender } = renderHook(() => useEventBus('stable-event', callback));
const firstCallbackRef = mockEventBus.on.mock.calls[0][1];
// Force a rerender
rerender();
// The callback passed to eventBus.on should remain the same
// (no re-subscription means the same callback is used)
expect(mockEventBus.on).toHaveBeenCalledTimes(1);
// Verify the callback still works after rerender
firstCallbackRef({ data: 'test' });
expect(callback).toHaveBeenCalledWith({ data: 'test' });
});
});
describe('cleanup timing', () => {
it('should unsubscribe before component is fully unmounted', () => {
const callback = vi.fn();
const cleanupOrder: string[] = [];
// Override off to track when it's called
mockEventBus.off.mockImplementation(() => {
cleanupOrder.push('eventBus.off');
});
const { unmount } = renderHook(() => useEventBus('cleanup-event', callback));
cleanupOrder.push('before unmount');
unmount();
cleanupOrder.push('after unmount');
expect(cleanupOrder).toEqual(['before unmount', 'eventBus.off', 'after unmount']);
});
});
});