Files
gridpilot.gg/apps/companion/main/automation/infrastructure/ElectronCheckoutConfirmationAdapter.test.ts

222 lines
6.5 KiB
TypeScript

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 '@core/automation/infrastructure//ipc/ElectronCheckoutConfirmationAdapter';
import { CheckoutPrice } from 'apps/companion/main/automation/domain/value-objects/CheckoutPrice';
import { CheckoutState } from 'apps/companion/main/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',
})
);
});
});
});