// Mock dependencies first const mockOnCustomEvent = jest.fn(); const mockNotifyCustomEvent = jest.fn(); const mockUuidv4 = jest.fn(); jest.mock('@jolibox/common', () => ({ uuidv4: mockUuidv4, EventEmitter: jest.fn().mockImplementation(() => ({ emit: jest.fn(), on: jest.fn(), off: jest.fn(), once: jest.fn(), removeAllListeners: jest.fn() })), InternalGlobalJSError: jest.fn().mockImplementation((error) => error) })); jest.mock('@/common/utils', () => ({ onCustomEvent: mockOnCustomEvent, notifyCustomEvent: mockNotifyCustomEvent })); jest.mock('@/common/report/errors/report', () => ({ reportError: jest.fn() })); import { getUserSubStatus, resetSubState } from '../sub'; // Define event data interface interface EventData { sequenceId: string; isSubUser: boolean; userId?: string; } describe('getUserSubStatus', () => { beforeEach(() => { jest.clearAllMocks(); jest.clearAllTimers(); jest.useRealTimers(); // Use real timers to avoid timing issues // Reset module state between tests resetSubState(); }); afterEach(() => { jest.restoreAllMocks(); }); it('should resolve with correct value when event is received', async () => { let eventHandler: ((data: EventData) => void) | null = null; mockOnCustomEvent.mockImplementation((eventName: string, handler: (data: EventData) => void) => { if (eventName === 'ON_JOLIBOX_GET_USER_SUB_STATUS') { eventHandler = handler; } return jest.fn(); }); mockUuidv4.mockReturnValue('test-sequence-id'); const promise = getUserSubStatus(); // Verify notification was sent expect(mockNotifyCustomEvent).toHaveBeenCalledWith('JOLIBOX_GET_USER_SUB_STATUS', { sequenceId: 'test-sequence-id' }); // Simulate response immediately if (eventHandler) { (eventHandler as any)({ sequenceId: 'test-sequence-id', isSubUser: true, userId: 'user-123' }); } const result = await promise; expect(result).toEqual({ isSubUser: true, userId: 'user-123' }); }); it('should resolve with false when isSubUser is false', async () => { let eventHandler: ((data: EventData) => void) | null = null; mockOnCustomEvent.mockImplementation((eventName: string, handler: (data: EventData) => void) => { if (eventName === 'ON_JOLIBOX_GET_USER_SUB_STATUS') { eventHandler = handler; } return jest.fn(); }); mockUuidv4.mockReturnValue('test-sequence-id'); const promise = getUserSubStatus(); // Simulate response with false if (eventHandler) { (eventHandler as any)({ sequenceId: 'test-sequence-id', isSubUser: false }); } const result = await promise; expect(result).toEqual({ isSubUser: false, userId: undefined }); }); it('should reject on timeout', async () => { jest.useFakeTimers(); mockOnCustomEvent.mockImplementation(() => jest.fn()); mockUuidv4.mockReturnValue('timeout-test-id'); const promise = getUserSubStatus(); // Fast forward time to trigger timeout jest.advanceTimersByTime(3000); await expect(promise).rejects.toThrow('Timeout waiting for user sub status response'); jest.useRealTimers(); }); it('should initialize event listener only once', async () => { let eventHandler: ((data: EventData) => void) | null = null; mockOnCustomEvent.mockImplementation((eventName: string, handler: (data: EventData) => void) => { if (eventName === 'ON_JOLIBOX_GET_USER_SUB_STATUS') { eventHandler = handler; } return jest.fn(); }); // Mock different UUIDs for different calls mockUuidv4.mockReturnValueOnce('uuid-1').mockReturnValueOnce('uuid-2'); // First call const promise1 = getUserSubStatus(); // Second call const promise2 = getUserSubStatus(); // Should only initialize listener once expect(mockOnCustomEvent).toHaveBeenCalledTimes(1); expect(mockOnCustomEvent).toHaveBeenCalledWith('ON_JOLIBOX_GET_USER_SUB_STATUS', expect.any(Function)); // Should send notification for both calls expect(mockNotifyCustomEvent).toHaveBeenCalledTimes(2); // Simulate responses with different sequence IDs if (eventHandler) { (eventHandler as any)({ sequenceId: 'uuid-1', isSubUser: true, userId: 'user-1' }); (eventHandler as any)({ sequenceId: 'uuid-2', isSubUser: false }); } const [result1, result2] = await Promise.all([promise1, promise2]); expect(result1).toEqual({ isSubUser: true, userId: 'user-1' }); expect(result2).toEqual({ isSubUser: false, userId: undefined }); }); it('should ignore responses with different sequenceId', async () => { let eventHandler: ((data: EventData) => void) | null = null; mockOnCustomEvent.mockImplementation((eventName: string, handler: (data: EventData) => void) => { if (eventName === 'ON_JOLIBOX_GET_USER_SUB_STATUS') { eventHandler = handler; } return jest.fn(); }); mockUuidv4.mockReturnValue('correct-sequence-id'); const promise = getUserSubStatus(); // Simulate response with wrong sequenceId first if (eventHandler) { (eventHandler as any)({ sequenceId: 'wrong-sequence-id', isSubUser: true, userId: 'user-wrong' }); } // Promise should still be pending after wrong sequenceId let isResolved = false; let promiseResult: any; promise .then((result) => { isResolved = true; promiseResult = result; }) .catch(() => { isResolved = true; }); // Flush microtasks await Promise.resolve(); expect(isResolved).toBe(false); // Now send correct response if (eventHandler) { (eventHandler as any)({ sequenceId: 'correct-sequence-id', isSubUser: true, userId: 'user-correct' }); } await promise; expect(promiseResult).toEqual({ isSubUser: true, userId: 'user-correct' }); }); it('should handle multiple concurrent requests with different sequenceIds', async () => { let eventHandler: ((data: EventData) => void) | null = null; mockOnCustomEvent.mockImplementation((eventName: string, handler: (data: EventData) => void) => { if (eventName === 'ON_JOLIBOX_GET_USER_SUB_STATUS') { eventHandler = handler; } return jest.fn(); }); // Mock different UUIDs for different calls mockUuidv4 .mockReturnValueOnce('request-1') .mockReturnValueOnce('request-2') .mockReturnValueOnce('request-3'); const promise1 = getUserSubStatus(); const promise2 = getUserSubStatus(); const promise3 = getUserSubStatus(); // Should initialize listener only once expect(mockOnCustomEvent).toHaveBeenCalledTimes(1); // Should send three notifications expect(mockNotifyCustomEvent).toHaveBeenCalledTimes(3); expect(mockNotifyCustomEvent).toHaveBeenNthCalledWith(1, 'JOLIBOX_GET_USER_SUB_STATUS', { sequenceId: 'request-1' }); expect(mockNotifyCustomEvent).toHaveBeenNthCalledWith(2, 'JOLIBOX_GET_USER_SUB_STATUS', { sequenceId: 'request-2' }); expect(mockNotifyCustomEvent).toHaveBeenNthCalledWith(3, 'JOLIBOX_GET_USER_SUB_STATUS', { sequenceId: 'request-3' }); // Simulate responses in reverse order if (eventHandler) { (eventHandler as any)({ sequenceId: 'request-3', isSubUser: false }); (eventHandler as any)({ sequenceId: 'request-1', isSubUser: true, userId: 'user-1' }); (eventHandler as any)({ sequenceId: 'request-2', isSubUser: true, userId: 'user-2' }); } const [result1, result2, result3] = await Promise.all([promise1, promise2, promise3]); expect(result1).toEqual({ isSubUser: true, userId: 'user-1' }); expect(result2).toEqual({ isSubUser: true, userId: 'user-2' }); expect(result3).toEqual({ isSubUser: false, userId: undefined }); }); it('should clean up timeout when response is received', async () => { let eventHandler: ((data: EventData) => void) | null = null; mockOnCustomEvent.mockImplementation((eventName: string, handler: (data: EventData) => void) => { if (eventName === 'ON_JOLIBOX_GET_USER_SUB_STATUS') { eventHandler = handler; } return jest.fn(); }); mockUuidv4.mockReturnValue('cleanup-test-id'); // Spy on clearTimeout const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout'); const promise = getUserSubStatus(); // Simulate response before timeout if (eventHandler) { (eventHandler as any)({ sequenceId: 'cleanup-test-id', isSubUser: true, userId: 'user-cleanup' }); } await promise; // Verify timeout was cleared expect(clearTimeoutSpy).toHaveBeenCalled(); clearTimeoutSpy.mockRestore(); }); it('should not resolve after timeout even if response comes later', async () => { jest.useFakeTimers(); let eventHandler: ((data: EventData) => void) | null = null; mockOnCustomEvent.mockImplementation((eventName: string, handler: (data: EventData) => void) => { if (eventName === 'ON_JOLIBOX_GET_USER_SUB_STATUS') { eventHandler = handler; } return jest.fn(); }); mockUuidv4.mockReturnValue('late-response-id'); const promise = getUserSubStatus(); // Trigger timeout first jest.advanceTimersByTime(3000); await expect(promise).rejects.toThrow('Timeout waiting for user sub status response'); // Try to send response after timeout if (eventHandler) { (eventHandler as any)({ sequenceId: 'late-response-id', isSubUser: true, userId: 'user-late' }); } jest.useRealTimers(); }); });