390 lines
13 KiB
TypeScript
390 lines
13 KiB
TypeScript
/**
|
||
* 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<number, string> = {
|
||
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<boolean> {
|
||
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('<!DOCTYPE html>');
|
||
});
|
||
|
||
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);
|
||
});
|
||
}); |