222 lines
6.5 KiB
TypeScript
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',
|
|
})
|
|
);
|
|
});
|
|
});
|
|
}); |