import { describe, it, expect, vi, beforeEach } from 'vitest'; import type { BrowserWindow } from 'electron'; // Mock electron module with factory function vi.mock('electron', () => ({ ipcMain: { on: vi.fn(), removeAllListeners: vi.fn(), }, })); import { ElectronCheckoutConfirmationAdapter } from '@gridpilot/automation/infrastructure/adapters/ipc/ElectronCheckoutConfirmationAdapter'; import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice'; import { CheckoutState } from '@gridpilot/automation/domain/value-objects/CheckoutState'; import { ipcMain } from 'electron'; describe('ElectronCheckoutConfirmationAdapter', () => { let mockWindow: BrowserWindow; let adapter: ElectronCheckoutConfirmationAdapter; type IpcEventLike = { sender?: unknown }; let ipcMainOnCallback: ((event: IpcEventLike, decision: 'confirmed' | 'cancelled' | 'timeout') => void) | null = null; beforeEach(() => { vi.clearAllMocks(); ipcMainOnCallback = null; // Capture the IPC handler callback vi.mocked(ipcMain.on).mockImplementation((channel, callback) => { if (channel === 'checkout:confirm') { ipcMainOnCallback = callback as (event: IpcEventLike, decision: 'confirmed' | 'cancelled' | 'timeout') => void; } return ipcMain; }); mockWindow = { webContents: { send: vi.fn(), }, } as unknown as BrowserWindow; adapter = new ElectronCheckoutConfirmationAdapter(mockWindow); }); describe('requestCheckoutConfirmation', () => { it('should send IPC message to renderer with request details', async () => { const request = { price: CheckoutPrice.fromString('$25.50'), state: CheckoutState.ready(), sessionMetadata: { sessionName: 'Test Race', trackId: 'spa', carIds: ['car1', 'car2'], }, timeoutMs: 30000, }; // Simulate immediate confirmation via IPC setTimeout(() => { if (ipcMainOnCallback) { ipcMainOnCallback({} as IpcEventLike, 'confirmed'); } }, 10); const result = await adapter.requestCheckoutConfirmation(request); expect(mockWindow.webContents.send).toHaveBeenCalledWith( 'checkout:request-confirmation', expect.objectContaining({ price: '$25.50', sessionMetadata: request.sessionMetadata, timeoutMs: 30000, }) ); expect(result.isOk()).toBe(true); const confirmation = result.unwrap(); expect(confirmation.isConfirmed()).toBe(true); }); it('should handle user confirmation', async () => { const request = { price: CheckoutPrice.fromString('$10.00'), state: CheckoutState.ready(), sessionMetadata: { sessionName: 'Test', trackId: 'spa', carIds: ['car1'], }, timeoutMs: 30000, }; setTimeout(() => { if (ipcMainOnCallback) { ipcMainOnCallback({} as IpcEventLike, 'confirmed'); } }, 10); const result = await adapter.requestCheckoutConfirmation(request); expect(result.isOk()).toBe(true); const confirmation = result.unwrap(); expect(confirmation.isConfirmed()).toBe(true); }); it('should handle user cancellation', async () => { const request = { price: CheckoutPrice.fromString('$10.00'), state: CheckoutState.ready(), sessionMetadata: { sessionName: 'Test', trackId: 'spa', carIds: ['car1'], }, timeoutMs: 30000, }; setTimeout(() => { if (ipcMainOnCallback) { ipcMainOnCallback({} as IpcEventLike, 'cancelled'); } }, 10); const result = await adapter.requestCheckoutConfirmation(request); expect(result.isOk()).toBe(true); const confirmation = result.unwrap(); expect(confirmation.isCancelled()).toBe(true); }); it('should timeout when no response received', async () => { const request = { price: CheckoutPrice.fromString('$10.00'), state: CheckoutState.ready(), sessionMetadata: { sessionName: 'Test', trackId: 'spa', carIds: ['car1'], }, timeoutMs: 100, }; const result = await adapter.requestCheckoutConfirmation(request); expect(result.isOk()).toBe(true); const confirmation = result.unwrap(); expect(confirmation.isTimeout()).toBe(true); }); it('should reject when already pending', async () => { const request = { price: CheckoutPrice.fromString('$10.00'), state: CheckoutState.ready(), sessionMetadata: { sessionName: 'Test', trackId: 'spa', carIds: ['car1'], }, timeoutMs: 30000, }; // Start first request const promise1 = adapter.requestCheckoutConfirmation(request); // Try to start second request immediately (should fail) const result2 = await adapter.requestCheckoutConfirmation(request); expect(result2.isErr()).toBe(true); expect(result2.unwrapErr().message).toContain('already pending'); // Confirm first request to clean up if (ipcMainOnCallback) { ipcMainOnCallback({} as IpcEventLike, 'confirmed'); } await promise1; }); it('should send correct state to renderer', async () => { const request = { price: CheckoutPrice.fromString('$10.00'), state: CheckoutState.ready(), sessionMetadata: { sessionName: 'Test', trackId: 'spa', carIds: ['car1'], }, timeoutMs: 100, }; await adapter.requestCheckoutConfirmation(request); expect(mockWindow.webContents.send).toHaveBeenCalledWith( 'checkout:request-confirmation', expect.objectContaining({ state: 'ready', }) ); }); it('should handle insufficient funds state', async () => { const request = { price: CheckoutPrice.fromString('$10.00'), state: CheckoutState.insufficientFunds(), sessionMetadata: { sessionName: 'Test', trackId: 'spa', carIds: ['car1'], }, timeoutMs: 100, }; await adapter.requestCheckoutConfirmation(request); expect(mockWindow.webContents.send).toHaveBeenCalledWith( 'checkout:request-confirmation', expect.objectContaining({ state: 'insufficient_funds', }) ); }); }); });