Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 2m15s
312 lines
9.4 KiB
TypeScript
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']);
|
|
});
|
|
});
|
|
});
|