/** * Docker-Based E2E Tests for BrowserDevToolsAdapter * * These tests run against real Docker containers: * - browserless/chrome: Headless Chrome with CDP exposed * - fixture-server: nginx serving static HTML fixtures * * Prerequisites: * - Run `npm run docker:e2e:up` to start containers * - Chrome available at ws://localhost:9222 * - Fixtures available at http://localhost:3456 * * IMPORTANT: These tests use REAL adapters, no mocks. */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { BrowserDevToolsAdapter } from '../../../packages/infrastructure/adapters/automation/BrowserDevToolsAdapter'; import { StepId } from '../../../packages/domain/value-objects/StepId'; import { IRacingSelectorMap, getStepSelectors, getStepName, } from '../../../packages/infrastructure/adapters/automation/selectors/IRacingSelectorMap'; // Environment configuration const CHROME_WS_ENDPOINT = process.env.CHROME_WS_ENDPOINT || 'ws://localhost:9222'; const FIXTURE_BASE_URL = process.env.FIXTURE_BASE_URL || 'http://localhost:3456'; const DEFAULT_TIMEOUT = 30000; // Map step numbers to fixture filenames const STEP_TO_FIXTURE: Record = { 2: '01-hosted-racing.html', 3: '02-create-a-race.html', 4: '03-race-information.html', 5: '04-server-details.html', 6: '05-set-admins.html', 7: '07-time-limits.html', 8: '08-set-cars.html', 9: '09-add-a-car.html', 10: '10-set-car-classes.html', 11: '11-set-track.html', 12: '12-add-a-track.html', 13: '13-track-options.html', 14: '14-time-of-day.html', 15: '15-weather.html', 16: '16-race-options.html', 17: '17-team-driving.html', 18: '18-track-conditions.html', }; /** * Helper to check if Docker environment is available */ async function isDockerEnvironmentReady(): Promise { try { // Check if Chrome CDP is accessible const chromeResponse = await fetch('http://localhost:9222/json/version'); if (!chromeResponse.ok) return false; // Check if fixture server is accessible const fixtureResponse = await fetch(`${FIXTURE_BASE_URL}/01-hosted-racing.html`); if (!fixtureResponse.ok) return false; return true; } catch { return false; } } describe('E2E: BrowserDevToolsAdapter - Docker Environment', () => { let adapter: BrowserDevToolsAdapter; let dockerReady: boolean; beforeAll(async () => { // Check if Docker environment is available dockerReady = await isDockerEnvironmentReady(); if (!dockerReady) { console.warn( '\n⚠️ Docker E2E environment not ready.\n' + ' Run: npm run docker:e2e:up\n' + ' Skipping Docker E2E tests.\n' ); return; } // Create adapter with CDP connection to Docker Chrome adapter = new BrowserDevToolsAdapter({ browserWSEndpoint: CHROME_WS_ENDPOINT, defaultTimeout: DEFAULT_TIMEOUT, typingDelay: 10, // Fast typing for tests waitForNetworkIdle: false, // Static fixtures don't need network idle }); await adapter.connect(); }, 60000); afterAll(async () => { if (adapter?.isConnected()) { await adapter.disconnect(); } }); // ==================== Connection Tests ==================== describe('Browser Connection', () => { it('should connect to Docker Chrome via CDP', () => { if (!dockerReady) return; expect(adapter.isConnected()).toBe(true); }); it('should have a valid page after connection', () => { if (!dockerReady) return; const url = adapter.getCurrentUrl(); expect(url).toBeDefined(); }); }); // ==================== Navigation Tests ==================== describe('Navigation to Fixtures', () => { it('should navigate to hosted racing page (step 2 fixture)', async () => { if (!dockerReady) return; const fixtureUrl = `${FIXTURE_BASE_URL}/${STEP_TO_FIXTURE[2]}`; const result = await adapter.navigateToPage(fixtureUrl); expect(result.success).toBe(true); expect(result.url).toBe(fixtureUrl); expect(result.loadTime).toBeGreaterThan(0); }); it('should navigate to race information page (step 4 fixture)', async () => { if (!dockerReady) return; const fixtureUrl = `${FIXTURE_BASE_URL}/${STEP_TO_FIXTURE[4]}`; const result = await adapter.navigateToPage(fixtureUrl); expect(result.success).toBe(true); expect(result.url).toBe(fixtureUrl); }); it('should return error for non-existent page', async () => { if (!dockerReady) return; const result = await adapter.navigateToPage(`${FIXTURE_BASE_URL}/nonexistent.html`); // Navigation may succeed but page returns 404 expect(result.success).toBe(true); // HTTP navigation succeeds }); }); // ==================== Element Detection Tests ==================== describe('Element Detection in Fixtures', () => { it('should detect elements exist on hosted racing page', async () => { if (!dockerReady) return; await adapter.navigateToPage(`${FIXTURE_BASE_URL}/${STEP_TO_FIXTURE[2]}`); // Wait for any element to verify page loaded const result = await adapter.waitForElement('body', 5000); expect(result.success).toBe(true); expect(result.found).toBe(true); }); it('should detect form inputs on race information page', async () => { if (!dockerReady) return; await adapter.navigateToPage(`${FIXTURE_BASE_URL}/${STEP_TO_FIXTURE[4]}`); // Check for input elements const bodyResult = await adapter.waitForElement('body', 5000); expect(bodyResult.success).toBe(true); // Evaluate page content to verify inputs exist const hasInputs = await adapter.evaluate(() => { return document.querySelectorAll('input').length > 0; }); expect(hasInputs).toBe(true); }); it('should return not found for non-existent element', async () => { if (!dockerReady) return; await adapter.navigateToPage(`${FIXTURE_BASE_URL}/${STEP_TO_FIXTURE[4]}`); const result = await adapter.waitForElement('#completely-nonexistent-element-xyz', 1000); expect(result.success).toBe(false); expect(result.found).toBe(false); }); }); // ==================== Click Operation Tests ==================== describe('Click Operations', () => { it('should click on visible elements', async () => { if (!dockerReady) return; await adapter.navigateToPage(`${FIXTURE_BASE_URL}/${STEP_TO_FIXTURE[4]}`); // Try to click any button if available const hasButtons = await adapter.evaluate(() => { return document.querySelectorAll('button').length > 0; }); if (hasButtons) { const result = await adapter.clickElement('button'); // May fail if button not visible/clickable, but should not throw expect(result).toBeDefined(); } }); it('should return error when clicking non-existent element', async () => { if (!dockerReady) return; await adapter.navigateToPage(`${FIXTURE_BASE_URL}/${STEP_TO_FIXTURE[4]}`); const result = await adapter.clickElement('#nonexistent-button'); expect(result.success).toBe(false); expect(result.error).toBeDefined(); }); }); // ==================== Form Fill Tests ==================== describe('Form Field Operations', () => { it('should fill form field when input exists', async () => { if (!dockerReady) return; await adapter.navigateToPage(`${FIXTURE_BASE_URL}/${STEP_TO_FIXTURE[4]}`); // Find first input on page const hasInput = await adapter.evaluate(() => { const input = document.querySelector('input[type="text"], input:not([type])'); return input !== null; }); if (hasInput) { const result = await adapter.fillFormField('input[type="text"], input:not([type])', 'Test Value'); // May succeed or fail depending on input visibility expect(result).toBeDefined(); } }); it('should return error when filling non-existent field', async () => { if (!dockerReady) return; await adapter.navigateToPage(`${FIXTURE_BASE_URL}/${STEP_TO_FIXTURE[4]}`); const result = await adapter.fillFormField('#nonexistent-input', 'Test Value'); expect(result.success).toBe(false); expect(result.error).toBeDefined(); }); }); // ==================== Step Execution Tests ==================== describe('Step Execution', () => { it('should execute step 1 (LOGIN) as skipped', async () => { if (!dockerReady) return; const stepId = StepId.create(1); if (stepId.isFailure()) throw new Error('Invalid step ID'); const result = await adapter.executeStep(stepId.value, {}); expect(result.success).toBe(true); expect(result.metadata?.skipped).toBe(true); expect(result.metadata?.reason).toBe('User pre-authenticated'); }); it('should execute step 18 (TRACK_CONDITIONS) with safety stop', async () => { if (!dockerReady) return; await adapter.navigateToPage(`${FIXTURE_BASE_URL}/${STEP_TO_FIXTURE[18]}`); const stepId = StepId.create(18); if (stepId.isFailure()) throw new Error('Invalid step ID'); const result = await adapter.executeStep(stepId.value, {}); expect(result.success).toBe(true); expect(result.metadata?.safetyStop).toBe(true); expect(result.metadata?.step).toBe('TRACK_CONDITIONS'); }); }); // ==================== Selector Validation Tests ==================== describe('IRacingSelectorMap Validation', () => { it('should have valid selectors that can be parsed by browser', async () => { if (!dockerReady) return; await adapter.navigateToPage(`${FIXTURE_BASE_URL}/${STEP_TO_FIXTURE[4]}`); // Test common selectors for CSS validity const selectorsToTest = [ IRacingSelectorMap.common.mainModal, IRacingSelectorMap.common.wizardContainer, IRacingSelectorMap.common.nextButton, ]; for (const selector of selectorsToTest) { const isValid = await adapter.evaluate((sel) => { try { document.querySelector(sel); return true; } catch { return false; } }, selector as unknown as () => boolean); // We're testing selector syntax, not element presence expect(typeof selector).toBe('string'); } }); it('should have selectors defined for all 18 steps', () => { for (let step = 1; step <= 18; step++) { const selectors = getStepSelectors(step); expect(selectors).toBeDefined(); expect(getStepName(step)).toBeDefined(); } }); }); // ==================== Page Content Tests ==================== describe('Page Content Retrieval', () => { it('should get page content from fixtures', async () => { if (!dockerReady) return; await adapter.navigateToPage(`${FIXTURE_BASE_URL}/${STEP_TO_FIXTURE[4]}`); const content = await adapter.getPageContent(); expect(content).toBeDefined(); expect(content.length).toBeGreaterThan(0); expect(content).toContain(''); }); it('should evaluate JavaScript in page context', async () => { if (!dockerReady) return; await adapter.navigateToPage(`${FIXTURE_BASE_URL}/${STEP_TO_FIXTURE[4]}`); const result = await adapter.evaluate(() => { return { title: document.title, hasBody: document.body !== null, }; }); expect(result.hasBody).toBe(true); }); }); // ==================== Workflow Progression Tests ==================== describe('Workflow Navigation', () => { it('should navigate through multiple fixtures sequentially', async () => { if (!dockerReady) return; // Navigate through first few steps const steps = [2, 3, 4, 5]; for (const step of steps) { const fixture = STEP_TO_FIXTURE[step]; if (!fixture) continue; const result = await adapter.navigateToPage(`${FIXTURE_BASE_URL}/${fixture}`); expect(result.success).toBe(true); // Verify page loaded const bodyExists = await adapter.waitForElement('body', 5000); expect(bodyExists.found).toBe(true); } }); }); }); // ==================== Standalone Skip Test ==================== describe('E2E Docker Environment Check', () => { it('should report Docker environment status', async () => { const ready = await isDockerEnvironmentReady(); if (ready) { console.log('✅ Docker E2E environment is ready'); } else { console.log('⚠️ Docker E2E environment not available'); console.log(' Start with: npm run docker:e2e:up'); } // This test always passes - it's informational expect(true).toBe(true); }); });