import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { BrowserDevToolsAdapter, DevToolsConfig } from '../../../packages/infrastructure/adapters/automation/BrowserDevToolsAdapter'; import { StepId } from '../../../packages/domain/value-objects/StepId'; import { IRacingSelectorMap, getStepSelectors, getStepName, isModalStep, } from '../../../packages/infrastructure/adapters/automation/selectors/IRacingSelectorMap'; // Mock puppeteer-core vi.mock('puppeteer-core', () => { const mockPage = { url: vi.fn().mockReturnValue('https://members-ng.iracing.com/web/racing/hosted'), goto: vi.fn().mockResolvedValue(undefined), $: vi.fn().mockResolvedValue({ click: vi.fn().mockResolvedValue(undefined), type: vi.fn().mockResolvedValue(undefined), }), click: vi.fn().mockResolvedValue(undefined), type: vi.fn().mockResolvedValue(undefined), waitForSelector: vi.fn().mockResolvedValue(undefined), setDefaultTimeout: vi.fn(), screenshot: vi.fn().mockResolvedValue(undefined), content: vi.fn().mockResolvedValue(''), waitForNetworkIdle: vi.fn().mockResolvedValue(undefined), evaluate: vi.fn().mockResolvedValue(undefined), }; const mockBrowser = { pages: vi.fn().mockResolvedValue([mockPage]), disconnect: vi.fn(), }; return { default: { connect: vi.fn().mockResolvedValue(mockBrowser), }, }; }); // Mock global fetch for CDP endpoint discovery global.fetch = vi.fn().mockResolvedValue({ json: vi.fn().mockResolvedValue({ webSocketDebuggerUrl: 'ws://127.0.0.1:9222/devtools/browser/mock-id', }), }); describe('BrowserDevToolsAdapter', () => { let adapter: BrowserDevToolsAdapter; beforeEach(() => { vi.clearAllMocks(); adapter = new BrowserDevToolsAdapter({ debuggingPort: 9222, defaultTimeout: 5000, typingDelay: 10, }); }); afterEach(async () => { if (adapter.isConnected()) { await adapter.disconnect(); } }); describe('instantiation', () => { it('should create adapter with default config', () => { const defaultAdapter = new BrowserDevToolsAdapter(); expect(defaultAdapter).toBeInstanceOf(BrowserDevToolsAdapter); expect(defaultAdapter.isConnected()).toBe(false); }); it('should create adapter with custom config', () => { const customConfig: DevToolsConfig = { debuggingPort: 9333, defaultTimeout: 10000, typingDelay: 100, waitForNetworkIdle: false, }; const customAdapter = new BrowserDevToolsAdapter(customConfig); expect(customAdapter).toBeInstanceOf(BrowserDevToolsAdapter); }); it('should create adapter with explicit WebSocket endpoint', () => { const wsAdapter = new BrowserDevToolsAdapter({ browserWSEndpoint: 'ws://127.0.0.1:9222/devtools/browser/test-id', }); expect(wsAdapter).toBeInstanceOf(BrowserDevToolsAdapter); }); }); describe('connect/disconnect', () => { it('should connect to browser via debugging port', async () => { await adapter.connect(); expect(adapter.isConnected()).toBe(true); }); it('should disconnect from browser without closing it', async () => { await adapter.connect(); expect(adapter.isConnected()).toBe(true); await adapter.disconnect(); expect(adapter.isConnected()).toBe(false); }); it('should handle multiple connect calls gracefully', async () => { await adapter.connect(); await adapter.connect(); // Should not throw expect(adapter.isConnected()).toBe(true); }); it('should handle disconnect when not connected', async () => { await adapter.disconnect(); // Should not throw expect(adapter.isConnected()).toBe(false); }); }); describe('navigateToPage', () => { beforeEach(async () => { await adapter.connect(); }); it('should navigate to URL successfully', async () => { const result = await adapter.navigateToPage('https://members-ng.iracing.com'); expect(result.success).toBe(true); expect(result.url).toBe('https://members-ng.iracing.com'); expect(result.loadTime).toBeGreaterThanOrEqual(0); }); it('should return error when not connected', async () => { await adapter.disconnect(); await expect(adapter.navigateToPage('https://example.com')) .rejects.toThrow('Not connected to browser'); }); }); describe('fillFormField', () => { beforeEach(async () => { await adapter.connect(); }); it('should fill form field successfully', async () => { const result = await adapter.fillFormField('input[name="sessionName"]', 'Test Session'); expect(result.success).toBe(true); expect(result.fieldName).toBe('input[name="sessionName"]'); expect(result.valueSet).toBe('Test Session'); }); it('should return error for non-existent field', async () => { // Re-mock to return null for element lookup const puppeteer = await import('puppeteer-core'); const mockBrowser = await puppeteer.default.connect({} as any); const pages = await mockBrowser.pages(); const mockPage = pages[0] as any; mockPage.$.mockResolvedValueOnce(null); const result = await adapter.fillFormField('input[name="nonexistent"]', 'value'); expect(result.success).toBe(false); expect(result.error).toContain('Field not found'); }); }); describe('clickElement', () => { beforeEach(async () => { await adapter.connect(); }); it('should click element successfully', async () => { const result = await adapter.clickElement('.btn-primary'); expect(result.success).toBe(true); expect(result.target).toBe('.btn-primary'); }); }); describe('waitForElement', () => { beforeEach(async () => { await adapter.connect(); }); it('should wait for element and find it', async () => { const result = await adapter.waitForElement('#create-race-modal', 5000); expect(result.success).toBe(true); expect(result.found).toBe(true); expect(result.target).toBe('#create-race-modal'); }); it('should return not found when element does not appear', async () => { // Re-mock to throw timeout error const puppeteer = await import('puppeteer-core'); const mockBrowser = await puppeteer.default.connect({} as any); const pages = await mockBrowser.pages(); const mockPage = pages[0] as any; mockPage.waitForSelector.mockRejectedValueOnce(new Error('Timeout')); const result = await adapter.waitForElement('#nonexistent', 100); expect(result.success).toBe(false); expect(result.found).toBe(false); }); }); describe('handleModal', () => { beforeEach(async () => { await adapter.connect(); }); it('should handle modal for step 6 (SET_ADMINS)', async () => { const stepId = StepId.create(6); const result = await adapter.handleModal(stepId, 'open'); expect(result.success).toBe(true); expect(result.stepId).toBe(6); expect(result.action).toBe('open'); }); it('should handle modal for step 9 (ADD_CAR)', async () => { const stepId = StepId.create(9); const result = await adapter.handleModal(stepId, 'close'); expect(result.success).toBe(true); expect(result.stepId).toBe(9); expect(result.action).toBe('close'); }); it('should handle modal for step 12 (ADD_TRACK)', async () => { const stepId = StepId.create(12); const result = await adapter.handleModal(stepId, 'search'); expect(result.success).toBe(true); expect(result.stepId).toBe(12); }); it('should return error for non-modal step', async () => { const stepId = StepId.create(4); // RACE_INFORMATION is not a modal step const result = await adapter.handleModal(stepId, 'open'); expect(result.success).toBe(false); expect(result.error).toContain('not a modal step'); }); it('should return error for unknown action', async () => { const stepId = StepId.create(6); const result = await adapter.handleModal(stepId, 'unknown_action'); expect(result.success).toBe(false); expect(result.error).toContain('Unknown modal action'); }); }); }); describe('IRacingSelectorMap', () => { describe('common selectors', () => { it('should have all required common selectors', () => { expect(IRacingSelectorMap.common.mainModal).toBeDefined(); expect(IRacingSelectorMap.common.modalDialog).toBeDefined(); expect(IRacingSelectorMap.common.modalContent).toBeDefined(); expect(IRacingSelectorMap.common.checkoutButton).toBeDefined(); expect(IRacingSelectorMap.common.wizardContainer).toBeDefined(); expect(IRacingSelectorMap.common.wizardSidebar).toBeDefined(); }); it('should have iRacing-specific URLs', () => { expect(IRacingSelectorMap.urls.base).toContain('iracing.com'); expect(IRacingSelectorMap.urls.hostedRacing).toContain('hosted'); }); }); describe('step selectors', () => { it('should have selectors for all 18 steps', () => { for (let i = 1; i <= 18; i++) { expect(IRacingSelectorMap.steps[i]).toBeDefined(); } }); it('should have wizard navigation for most steps', () => { // Steps that have wizard navigation const stepsWithWizardNav = [4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16, 18]; for (const stepNum of stepsWithWizardNav) { const selectors = IRacingSelectorMap.steps[stepNum]; expect(selectors.wizardNav || selectors.sidebarLink).toBeDefined(); } }); it('should have modal selectors for modal steps (6, 9, 12)', () => { expect(IRacingSelectorMap.steps[6].modal).toBeDefined(); expect(IRacingSelectorMap.steps[9].modal).toBeDefined(); expect(IRacingSelectorMap.steps[12].modal).toBeDefined(); }); it('should NOT have checkout button in step 18 (safety)', () => { const step18 = IRacingSelectorMap.steps[18]; expect(step18.buttons?.checkout).toBeUndefined(); }); }); describe('getStepSelectors', () => { it('should return selectors for valid step', () => { const selectors = getStepSelectors(4); expect(selectors).toBeDefined(); expect(selectors?.container).toBe('#set-session-information'); }); it('should return undefined for invalid step', () => { const selectors = getStepSelectors(99); expect(selectors).toBeUndefined(); }); }); describe('isModalStep', () => { it('should return true for modal steps', () => { expect(isModalStep(6)).toBe(true); expect(isModalStep(9)).toBe(true); expect(isModalStep(12)).toBe(true); }); it('should return false for non-modal steps', () => { expect(isModalStep(1)).toBe(false); expect(isModalStep(4)).toBe(false); expect(isModalStep(18)).toBe(false); }); }); describe('getStepName', () => { it('should return correct step names', () => { expect(getStepName(1)).toBe('LOGIN'); expect(getStepName(4)).toBe('RACE_INFORMATION'); expect(getStepName(6)).toBe('SET_ADMINS'); expect(getStepName(9)).toBe('ADD_CAR'); expect(getStepName(12)).toBe('ADD_TRACK'); expect(getStepName(18)).toBe('TRACK_CONDITIONS'); }); it('should return UNKNOWN for invalid step', () => { expect(getStepName(99)).toContain('UNKNOWN'); }); }); }); describe('Integration: Adapter with SelectorMap', () => { let adapter: BrowserDevToolsAdapter; beforeEach(async () => { adapter = new BrowserDevToolsAdapter(); await adapter.connect(); }); afterEach(async () => { await adapter.disconnect(); }); it('should use selector map for navigation', async () => { const selectors = getStepSelectors(4); expect(selectors?.sidebarLink).toBeDefined(); const result = await adapter.clickElement(selectors!.sidebarLink!); expect(result.success).toBe(true); }); it('should use selector map for form filling', async () => { const selectors = getStepSelectors(4); expect(selectors?.fields?.sessionName).toBeDefined(); const result = await adapter.fillFormField( selectors!.fields!.sessionName, 'My Test Session' ); expect(result.success).toBe(true); }); it('should use selector map for modal handling', async () => { const stepId = StepId.create(9); const selectors = getStepSelectors(9); expect(selectors?.modal).toBeDefined(); const result = await adapter.handleModal(stepId, 'open'); expect(result.success).toBe(true); }); });