UNPKG

bc-webclient-mcp

Version:

Model Context Protocol (MCP) server for Microsoft Dynamics 365 Business Central via WebUI protocol. Enables AI assistants to interact with BC through the web client protocol, supporting Card, List, and Document pages with full line item support and server

315 lines 13.7 kB
/** * AbortSignal Utilities Tests * * Tests for AbortSignal composition, timeout detection, and abort listeners. * Note: AbortSignal.timeout() uses real timers (cannot be faked), so tests * use short timeouts and manual AbortController for deterministic testing. */ import { describe, it, expect, vi } from 'vitest'; import { composeWithTimeout, isTimeoutAbortReason, isAbortError, wasExternallyAborted, onceAborted, } from './abort.js'; describe('abort', () => { describe('composeWithTimeout()', () => { it('returns timeout signal when parent is undefined', () => { // Arrange & Act const signal = composeWithTimeout(undefined, 100); // Assert expect(signal).toBeInstanceOf(AbortSignal); expect(signal.aborted).toBe(false); }); it('composes parent with timeout using AbortSignal.any()', () => { // Arrange const parent = new AbortController().signal; // Act const signal = composeWithTimeout(parent, 100); // Assert expect(signal).toBeInstanceOf(AbortSignal); expect(signal.aborted).toBe(false); }); it('aborts when parent signal aborts', async () => { // Arrange const parentController = new AbortController(); const signal = composeWithTimeout(parentController.signal, 1000); // Act parentController.abort(); // Assert expect(signal.aborted).toBe(true); expect(wasExternallyAborted(signal)).toBe(true); }); it('aborts when timeout expires', async () => { // Arrange & Act const signal = composeWithTimeout(undefined, 10); // Short timeout // Wait for timeout to expire await new Promise(resolve => setTimeout(resolve, 50)); // Assert expect(signal.aborted).toBe(true); expect(isTimeoutAbortReason(signal.reason)).toBe(true); }); it('aborts on parent abort before timeout', async () => { // Arrange const parentController = new AbortController(); const signal = composeWithTimeout(parentController.signal, 1000); // Act - abort parent immediately parentController.abort(new Error('User cancelled')); // Assert expect(signal.aborted).toBe(true); expect(isTimeoutAbortReason(signal.reason)).toBe(false); expect(wasExternallyAborted(signal)).toBe(true); }); it('aborts on timeout before parent abort', async () => { // Arrange const parentController = new AbortController(); const signal = composeWithTimeout(parentController.signal, 10); // Act - wait for timeout (don't abort parent) await new Promise(resolve => setTimeout(resolve, 50)); // Assert expect(signal.aborted).toBe(true); expect(isTimeoutAbortReason(signal.reason)).toBe(true); expect(wasExternallyAborted(signal)).toBe(false); }); it('works with already-aborted parent signal', () => { // Arrange const parentController = new AbortController(); parentController.abort(new Error('Already aborted')); // Act const signal = composeWithTimeout(parentController.signal, 1000); // Assert expect(signal.aborted).toBe(true); expect(wasExternallyAborted(signal)).toBe(true); }); }); describe('isTimeoutAbortReason()', () => { it('returns true for DOMException with name TimeoutError', () => { // Arrange const reason = new DOMException('Timeout', 'TimeoutError'); // Act & Assert expect(isTimeoutAbortReason(reason)).toBe(true); }); it('returns false for DOMException with different name', () => { // Arrange const reason = new DOMException('Aborted', 'AbortError'); // Act & Assert expect(isTimeoutAbortReason(reason)).toBe(false); }); it('returns false for Error objects', () => { // Arrange const reason = new Error('Cancelled'); // Act & Assert expect(isTimeoutAbortReason(reason)).toBe(false); }); it('returns false for plain objects without name property', () => { // Arrange const reason = { message: 'Aborted' }; // Act & Assert expect(isTimeoutAbortReason(reason)).toBe(false); }); it('returns false for null', () => { expect(isTimeoutAbortReason(null)).toBe(false); }); it('returns false for undefined', () => { expect(isTimeoutAbortReason(undefined)).toBe(false); }); it('returns false for primitive types', () => { expect(isTimeoutAbortReason('timeout')).toBe(false); expect(isTimeoutAbortReason(123)).toBe(false); expect(isTimeoutAbortReason(true)).toBe(false); }); it('returns false for objects with name property but wrong value', () => { // Arrange const reason = { name: 'SomeOtherError', message: 'Failed' }; // Act & Assert expect(isTimeoutAbortReason(reason)).toBe(false); }); }); describe('isAbortError()', () => { it('returns true for DOMException with name AbortError', () => { // Arrange const error = new DOMException('Aborted', 'AbortError'); // Act & Assert expect(isAbortError(error)).toBe(true); }); it('returns false for DOMException with different name', () => { // Arrange const error = new DOMException('Timeout', 'TimeoutError'); // Act & Assert expect(isAbortError(error)).toBe(false); }); it('returns false for regular Error objects', () => { // Arrange const error = new Error('Aborted'); // Act & Assert expect(isAbortError(error)).toBe(false); }); it('returns true for plain objects with name AbortError (duck typing)', () => { // Arrange - implementation uses duck typing, not instanceof const error = { name: 'AbortError', message: 'Aborted' }; // Act & Assert expect(isAbortError(error)).toBe(true); }); it('returns false for null', () => { expect(isAbortError(null)).toBe(false); }); it('returns false for undefined', () => { expect(isAbortError(undefined)).toBe(false); }); it('returns false for primitive types', () => { expect(isAbortError('error')).toBe(false); expect(isAbortError(123)).toBe(false); expect(isAbortError(false)).toBe(false); }); }); describe('wasExternallyAborted()', () => { it('returns false when signal is undefined', () => { expect(wasExternallyAborted(undefined)).toBe(false); }); it('returns false when signal is not aborted', () => { // Arrange const signal = new AbortController().signal; // Act & Assert expect(wasExternallyAborted(signal)).toBe(false); }); it('returns true when signal is aborted with non-timeout reason', () => { // Arrange const controller = new AbortController(); controller.abort(new Error('User cancelled')); // Act & Assert expect(wasExternallyAborted(controller.signal)).toBe(true); }); it('returns false when signal is aborted by timeout', async () => { // Arrange - create timeout signal const signal = AbortSignal.timeout(10); // Wait for timeout await new Promise(resolve => setTimeout(resolve, 50)); // Act & Assert expect(signal.aborted).toBe(true); expect(wasExternallyAborted(signal)).toBe(false); }); it('returns true when signal is aborted without reason', () => { // Arrange const controller = new AbortController(); controller.abort(); // No reason provided // Act & Assert expect(wasExternallyAborted(controller.signal)).toBe(true); }); it('returns true when signal is aborted with custom reason object', () => { // Arrange const controller = new AbortController(); controller.abort({ code: 'USER_CANCEL', message: 'Cancelled' }); // Act & Assert expect(wasExternallyAborted(controller.signal)).toBe(true); }); it('distinguishes external abort from timeout in composed signal', async () => { // Arrange const parentController = new AbortController(); const composed = composeWithTimeout(parentController.signal, 1000); // Act - external abort parentController.abort(new Error('User action')); // Assert expect(composed.aborted).toBe(true); expect(wasExternallyAborted(composed)).toBe(true); }); }); describe('onceAborted()', () => { it('calls callback when signal aborts', () => { // Arrange const controller = new AbortController(); const callback = vi.fn(); // Act onceAborted(controller.signal, callback); controller.abort(); // Assert expect(callback).toHaveBeenCalledTimes(1); }); it('does not call callback if signal never aborts', () => { // Arrange const controller = new AbortController(); const callback = vi.fn(); // Act onceAborted(controller.signal, callback); // Assert (no abort) expect(callback).not.toHaveBeenCalled(); }); it('calls callback only once even if abort is triggered multiple times', () => { // Arrange const controller = new AbortController(); const callback = vi.fn(); // Act onceAborted(controller.signal, callback); controller.abort(); // Try to abort again (AbortController ignores subsequent aborts) controller.abort(); // Assert expect(callback).toHaveBeenCalledTimes(1); }); it('returns cleanup function that removes listener', () => { // Arrange const controller = new AbortController(); const callback = vi.fn(); // Act const cleanup = onceAborted(controller.signal, callback); cleanup(); // Remove listener before abort controller.abort(); // Assert expect(callback).not.toHaveBeenCalled(); }); it('cleanup function is safe to call multiple times', () => { // Arrange const controller = new AbortController(); const callback = vi.fn(); // Act const cleanup = onceAborted(controller.signal, callback); cleanup(); cleanup(); // Call again // Assert - no errors, callback not called controller.abort(); expect(callback).not.toHaveBeenCalled(); }); it('cleanup function is safe to call after abort', () => { // Arrange const controller = new AbortController(); const callback = vi.fn(); // Act const cleanup = onceAborted(controller.signal, callback); controller.abort(); cleanup(); // Call after abort // Assert - callback was called once expect(callback).toHaveBeenCalledTimes(1); }); it('does not call callback for already-aborted signal', () => { // Arrange const controller = new AbortController(); controller.abort(); const callback = vi.fn(); // Act - attach listener to already-aborted signal onceAborted(controller.signal, callback); // Assert - addEventListener does not call listeners synchronously for already-aborted signals // The listener only handles future abort events, not past ones expect(callback).not.toHaveBeenCalled(); }); it('handles multiple listeners on same signal independently', () => { // Arrange const controller = new AbortController(); const callback1 = vi.fn(); const callback2 = vi.fn(); // Act const cleanup1 = onceAborted(controller.signal, callback1); onceAborted(controller.signal, callback2); cleanup1(); // Remove first listener controller.abort(); // Assert expect(callback1).not.toHaveBeenCalled(); expect(callback2).toHaveBeenCalledTimes(1); }); it('allows callback to be called when timeout signal aborts', async () => { // Arrange const signal = AbortSignal.timeout(10); const callback = vi.fn(); // Act onceAborted(signal, callback); // Wait for timeout await new Promise(resolve => setTimeout(resolve, 50)); // Assert expect(callback).toHaveBeenCalledTimes(1); }); }); }); //# sourceMappingURL=abort.spec.js.map