import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'; import { onShutdown, Shutdown, SHUTDOWN_DEFAULTS, } from '../src'; import type { ShutdownOptions } from '../src/types'; describe('Shutdown Comprehensive Tests', () => { let shutdownInstance: Shutdown; beforeEach(() => { // Reset singleton instance for each test (Shutdown as any).instance = null; // Create a fresh instance for each test shutdownInstance = new Shutdown({ autoRegister: false }); }); afterEach(() => { // Clean up singleton (Shutdown as any).instance = null; }); describe('Basic Functionality', () => { it('should register callbacks', () => { const callback = mock(async () => {}); shutdownInstance.onShutdown(callback, 50, 'test-callback'); // We can't directly check callback count, but we can verify it executes expect(callback).toBeDefined(); }); it('should execute callbacks on shutdown', async () => { const callback1 = mock(async () => {}); const callback2 = mock(async () => {}); shutdownInstance.onShutdown(callback1, 10, 'callback-1'); shutdownInstance.onShutdown(callback2, 20, 'callback-2'); await shutdownInstance.shutdown(); expect(callback1).toHaveBeenCalledTimes(1); expect(callback2).toHaveBeenCalledTimes(1); }); it('should execute callbacks in priority order', async () => { const order: string[] = []; const highPriority = mock(async () => { order.push('high'); }); const mediumPriority = mock(async () => { order.push('medium'); }); const lowPriority = mock(async () => { order.push('low'); }); // Lower number = higher priority shutdownInstance.onShutdown(lowPriority, 30, 'low'); shutdownInstance.onShutdown(highPriority, 10, 'high'); shutdownInstance.onShutdown(mediumPriority, 20, 'medium'); await shutdownInstance.shutdown(); expect(order).toEqual(['high', 'medium', 'low']); }); it('should handle errors in callbacks', async () => { const successCallback = mock(async () => {}); const errorCallback = mock(async () => { throw new Error('Callback error'); }); shutdownInstance.onShutdown(successCallback, 10, 'success'); shutdownInstance.onShutdown(errorCallback, 20, 'error'); // Should not throw even if callbacks fail await expect(shutdownInstance.shutdown()).resolves.toBeUndefined(); expect(successCallback).toHaveBeenCalledTimes(1); expect(errorCallback).toHaveBeenCalledTimes(1); }); it('should only shutdown once', async () => { const callback = mock(async () => {}); shutdownInstance.onShutdown(callback); await shutdownInstance.shutdown(); await shutdownInstance.shutdown(); await shutdownInstance.shutdown(); expect(callback).toHaveBeenCalledTimes(1); }); it('should not register callbacks during shutdown', async () => { const firstCallback = mock(async () => { // Try to register another callback during shutdown shutdownInstance.onShutdown(async () => { throw new Error('Should not execute'); }); }); shutdownInstance.onShutdown(firstCallback); await shutdownInstance.shutdown(); expect(firstCallback).toHaveBeenCalledTimes(1); }); }); describe('Timeout Handling', () => { it('should timeout if callbacks take too long', async () => { const slowShutdown = new Shutdown({ timeout: 100, autoRegister: false }); const slowCallback = mock(async () => { await new Promise(resolve => setTimeout(resolve, 200)); }); slowShutdown.onShutdown(slowCallback, 10, 'slow'); const startTime = Date.now(); await expect(slowShutdown.shutdown()).rejects.toThrow('Shutdown timeout'); const duration = Date.now() - startTime; expect(duration).toBeLessThan(150); }); it('should complete if callbacks finish before timeout', async () => { const quickShutdown = new Shutdown({ timeout: 1000, autoRegister: false }); const quickCallback = mock(async () => { await new Promise(resolve => setTimeout(resolve, 10)); }); quickShutdown.onShutdown(quickCallback); await expect(quickShutdown.shutdown()).resolves.toBeUndefined(); expect(quickCallback).toHaveBeenCalledTimes(1); }); }); describe('Singleton Pattern', () => { it('should return same instance via getInstance', () => { const instance1 = Shutdown.getInstance(); const instance2 = Shutdown.getInstance(); expect(instance1).toBe(instance2); }); it('should maintain state across getInstance calls', async () => { const instance = Shutdown.getInstance(); const callback = mock(async () => {}); instance.onShutdown(callback); const sameInstance = Shutdown.getInstance(); await sameInstance.shutdown(); expect(callback).toHaveBeenCalledTimes(1); }); }); describe('Global onShutdown Function', () => { it('should use singleton instance', async () => { const callback = mock(async () => {}); // Use global function onShutdown(callback, 50, 'global-callback'); // Get instance and shutdown const instance = Shutdown.getInstance(); await instance.shutdown(); expect(callback).toHaveBeenCalledTimes(1); }); it('should handle different parameter orders', () => { const callback = mock(async () => {}); // Test with all parameters onShutdown(callback, 10, 'with-all-params'); // Test with default priority onShutdown(callback); // Test with priority but no name onShutdown(callback, 20); expect(callback).toBeDefined(); }); }); describe('Edge Cases', () => { it('should handle synchronous callbacks', async () => { const syncCallback = mock(() => { // Synchronous callback }); shutdownInstance.onShutdown(syncCallback as any, 10, 'sync'); await shutdownInstance.shutdown(); expect(syncCallback).toHaveBeenCalledTimes(1); }); it('should handle callbacks that throw non-Error objects', async () => { const throwingCallback = mock(async () => { throw 'string error'; }); shutdownInstance.onShutdown(throwingCallback); // Should not throw await expect(shutdownInstance.shutdown()).resolves.toBeUndefined(); expect(throwingCallback).toHaveBeenCalledTimes(1); }); it('should handle empty callback list', async () => { // No callbacks registered await expect(shutdownInstance.shutdown()).resolves.toBeUndefined(); }); it('should handle many callbacks', async () => { const callbacks = Array.from({ length: 50 }, (_, i) => mock(async () => {}) ); callbacks.forEach((cb, i) => { shutdownInstance.onShutdown(cb, i, `callback-${i}`); }); await shutdownInstance.shutdown(); callbacks.forEach(cb => { expect(cb).toHaveBeenCalledTimes(1); }); }); }); describe('Default Values', () => { it('should use default priority', async () => { const callback = mock(async () => {}); // No priority specified shutdownInstance.onShutdown(callback); await shutdownInstance.shutdown(); expect(callback).toHaveBeenCalledTimes(1); }); it('should use default timeout', () => { const defaultShutdown = new Shutdown(); // Can't directly test timeout value, but we can verify it doesn't throw expect(() => new Shutdown()).not.toThrow(); }); }); });