refactor(automation): remove browser automation, use OS-level automation only
This commit is contained in:
@@ -1,422 +0,0 @@
|
||||
/**
|
||||
* E2E Tests for Automation Workflow
|
||||
*
|
||||
* IMPORTANT: These tests run in HEADLESS mode only.
|
||||
* No headed browser tests are allowed per project requirements.
|
||||
*
|
||||
* Tests verify that the IRacingSelectorMap selectors correctly find
|
||||
* elements in the static HTML fixture files.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
|
||||
import puppeteer, { Browser, Page } from 'puppeteer';
|
||||
import { createServer, Server } from 'http';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import {
|
||||
IRacingSelectorMap,
|
||||
getStepSelectors,
|
||||
getStepName,
|
||||
isModalStep,
|
||||
} from '../../packages/infrastructure/adapters/automation/selectors/IRacingSelectorMap';
|
||||
|
||||
// ==================== HEADLESS ENFORCEMENT ====================
|
||||
const HEADLESS_MODE = true; // NEVER change this to false - headless only!
|
||||
|
||||
// ==================== Test Configuration ====================
|
||||
const FIXTURES_DIR = join(process.cwd(), 'resources', 'iracing-hosted-sessions');
|
||||
const TEST_PORT = 3456;
|
||||
const TEST_BASE_URL = `http://localhost:${TEST_PORT}`;
|
||||
|
||||
// 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',
|
||||
};
|
||||
|
||||
// ==================== HTTP Server for Fixtures ====================
|
||||
function createFixtureServer(): Server {
|
||||
return createServer((req, res) => {
|
||||
const url = req.url || '/';
|
||||
const filename = url.slice(1) || 'index.html';
|
||||
const filepath = join(FIXTURES_DIR, filename);
|
||||
|
||||
if (existsSync(filepath)) {
|
||||
const content = readFileSync(filepath, 'utf-8');
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(content);
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.end('Not Found');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Test Suite ====================
|
||||
describe('E2E: Automation Workflow - HEADLESS MODE', () => {
|
||||
let browser: Browser;
|
||||
let page: Page;
|
||||
let server: Server;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Start fixture server
|
||||
server = createFixtureServer();
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(TEST_PORT, () => {
|
||||
console.log(`Fixture server running on port ${TEST_PORT}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// Launch browser in HEADLESS mode - this is enforced and cannot be changed
|
||||
browser = await puppeteer.launch({
|
||||
headless: HEADLESS_MODE, // MUST be true - never run headed
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-gpu',
|
||||
],
|
||||
});
|
||||
}, 30000);
|
||||
|
||||
afterAll(async () => {
|
||||
// Close browser
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
// Stop fixture server
|
||||
if (server) {
|
||||
await new Promise<void>((resolve) => {
|
||||
server.close(() => resolve());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
page = await browser.newPage();
|
||||
// Set viewport for consistent rendering
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (page) {
|
||||
await page.close();
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== Headless Mode Verification ====================
|
||||
describe('Headless Mode Verification', () => {
|
||||
it('should be running in headless mode', async () => {
|
||||
// This test verifies we're in headless mode
|
||||
expect(HEADLESS_MODE).toBe(true);
|
||||
|
||||
// Additional check - headless browsers have specific user agent patterns
|
||||
const userAgent = await page.evaluate(() => navigator.userAgent);
|
||||
// Headless Chrome includes "HeadlessChrome" in newer versions
|
||||
// or we can verify by checking we can't access certain headed-only features
|
||||
expect(browser.connected).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== IRacingSelectorMap Tests ====================
|
||||
describe('IRacingSelectorMap Structure', () => {
|
||||
it('should have all 18 steps defined', () => {
|
||||
for (let step = 1; step <= 18; step++) {
|
||||
const selectors = getStepSelectors(step);
|
||||
expect(selectors, `Step ${step} should have selectors`).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('should identify modal steps correctly', () => {
|
||||
expect(isModalStep(6)).toBe(true); // SET_ADMINS
|
||||
expect(isModalStep(9)).toBe(true); // ADD_CAR
|
||||
expect(isModalStep(12)).toBe(true); // ADD_TRACK
|
||||
expect(isModalStep(1)).toBe(false);
|
||||
expect(isModalStep(18)).toBe(false);
|
||||
});
|
||||
|
||||
it('should have correct step names', () => {
|
||||
expect(getStepName(1)).toBe('LOGIN');
|
||||
expect(getStepName(4)).toBe('RACE_INFORMATION');
|
||||
expect(getStepName(9)).toBe('ADD_CAR');
|
||||
expect(getStepName(18)).toBe('TRACK_CONDITIONS');
|
||||
});
|
||||
|
||||
it('should NOT have checkout button in final step (safety)', () => {
|
||||
const step18 = getStepSelectors(18);
|
||||
expect(step18?.buttons?.checkout).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should have common selectors defined', () => {
|
||||
expect(IRacingSelectorMap.common.mainModal).toBeDefined();
|
||||
expect(IRacingSelectorMap.common.checkoutButton).toBeDefined();
|
||||
expect(IRacingSelectorMap.common.wizardContainer).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== Fixture Loading Tests ====================
|
||||
describe('HTML Fixture Loading', () => {
|
||||
it('should load hosted racing fixture (step 2)', async () => {
|
||||
const fixture = STEP_TO_FIXTURE[2];
|
||||
if (!fixture) {
|
||||
console.log('Skipping: No fixture for step 2');
|
||||
return;
|
||||
}
|
||||
|
||||
await page.goto(`${TEST_BASE_URL}/${fixture}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
const title = await page.title();
|
||||
expect(title).toBeDefined();
|
||||
}, 60000);
|
||||
|
||||
it('should load race information fixture (step 4)', async () => {
|
||||
const fixture = STEP_TO_FIXTURE[4];
|
||||
if (!fixture) {
|
||||
console.log('Skipping: No fixture for step 4');
|
||||
return;
|
||||
}
|
||||
|
||||
await page.goto(`${TEST_BASE_URL}/${fixture}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
const content = await page.content();
|
||||
expect(content.length).toBeGreaterThan(0);
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
// ==================== Selector Validation Tests ====================
|
||||
describe('Selector Validation Against Fixtures', () => {
|
||||
// Test common selectors that should exist in the wizard pages
|
||||
const wizardSteps = [4, 5, 6, 7, 8, 10, 11, 13, 14, 15, 16, 17, 18];
|
||||
|
||||
for (const stepNum of wizardSteps) {
|
||||
const fixture = STEP_TO_FIXTURE[stepNum];
|
||||
if (!fixture) continue;
|
||||
|
||||
it(`should find wizard container in step ${stepNum} (${getStepName(stepNum)})`, async () => {
|
||||
await page.goto(`${TEST_BASE_URL}/${fixture}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
// Check for wizard-related elements using broad selectors
|
||||
// The actual iRacing selectors may use dynamic classes, so we test for presence of expected patterns
|
||||
const hasContent = await page.evaluate(() => {
|
||||
return document.body.innerHTML.length > 0;
|
||||
});
|
||||
|
||||
expect(hasContent).toBe(true);
|
||||
}, 120000);
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== Modal Step Tests ====================
|
||||
describe('Modal Step Fixtures', () => {
|
||||
it('should load add-an-admin fixture (step 6 modal)', async () => {
|
||||
const fixture = '06-add-an-admin.html';
|
||||
const filepath = join(FIXTURES_DIR, fixture);
|
||||
|
||||
if (!existsSync(filepath)) {
|
||||
console.log('Skipping: Modal fixture not available');
|
||||
return;
|
||||
}
|
||||
|
||||
await page.goto(`${TEST_BASE_URL}/${fixture}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
// Verify page loaded
|
||||
const content = await page.content();
|
||||
expect(content.length).toBeGreaterThan(1000);
|
||||
}, 120000);
|
||||
|
||||
it('should load add-a-car fixture (step 9 modal)', async () => {
|
||||
const fixture = STEP_TO_FIXTURE[9];
|
||||
if (!fixture) {
|
||||
console.log('Skipping: No fixture for step 9');
|
||||
return;
|
||||
}
|
||||
|
||||
await page.goto(`${TEST_BASE_URL}/${fixture}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
const content = await page.content();
|
||||
expect(content.length).toBeGreaterThan(1000);
|
||||
}, 120000);
|
||||
|
||||
it('should load add-a-track fixture (step 12 modal)', async () => {
|
||||
const fixture = STEP_TO_FIXTURE[12];
|
||||
if (!fixture) {
|
||||
console.log('Skipping: No fixture for step 12');
|
||||
return;
|
||||
}
|
||||
|
||||
await page.goto(`${TEST_BASE_URL}/${fixture}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
const content = await page.content();
|
||||
expect(content.length).toBeGreaterThan(1000);
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
// ==================== Workflow Progression Tests ====================
|
||||
describe('Workflow Step Progression', () => {
|
||||
it('should have fixtures for complete 18-step workflow', () => {
|
||||
// Verify we have fixtures for the workflow steps
|
||||
const availableSteps = Object.keys(STEP_TO_FIXTURE).map(Number);
|
||||
|
||||
// We should have fixtures for steps 2-18 (step 1 is login, handled externally)
|
||||
expect(availableSteps.length).toBeGreaterThanOrEqual(15);
|
||||
|
||||
// Check critical steps exist
|
||||
expect(STEP_TO_FIXTURE[4]).toBeDefined(); // RACE_INFORMATION
|
||||
expect(STEP_TO_FIXTURE[8]).toBeDefined(); // SET_CARS
|
||||
expect(STEP_TO_FIXTURE[11]).toBeDefined(); // SET_TRACK
|
||||
expect(STEP_TO_FIXTURE[18]).toBeDefined(); // TRACK_CONDITIONS
|
||||
});
|
||||
|
||||
it('should have step 18 as final step (safety checkpoint)', () => {
|
||||
const step18Selectors = getStepSelectors(18);
|
||||
|
||||
expect(step18Selectors).toBeDefined();
|
||||
expect(getStepName(18)).toBe('TRACK_CONDITIONS');
|
||||
|
||||
// Verify checkout is NOT automated (safety)
|
||||
expect(step18Selectors?.buttons?.checkout).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== Element Detection Tests ====================
|
||||
describe('Element Detection in Fixtures', () => {
|
||||
it('should detect form elements in race information page', async () => {
|
||||
const fixture = STEP_TO_FIXTURE[4];
|
||||
if (!fixture) {
|
||||
console.log('Skipping: No fixture');
|
||||
return;
|
||||
}
|
||||
|
||||
await page.goto(`${TEST_BASE_URL}/${fixture}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
// Check for input elements (the page should have form inputs)
|
||||
const hasInputs = await page.evaluate(() => {
|
||||
const inputs = document.querySelectorAll('input');
|
||||
return inputs.length > 0;
|
||||
});
|
||||
|
||||
expect(hasInputs).toBe(true);
|
||||
}, 120000);
|
||||
|
||||
it('should detect buttons in fixture pages', async () => {
|
||||
const fixture = STEP_TO_FIXTURE[5];
|
||||
if (!fixture) {
|
||||
console.log('Skipping: No fixture');
|
||||
return;
|
||||
}
|
||||
|
||||
await page.goto(`${TEST_BASE_URL}/${fixture}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
// Check for button elements
|
||||
const hasButtons = await page.evaluate(() => {
|
||||
const buttons = document.querySelectorAll('button, .btn, [role="button"]');
|
||||
return buttons.length > 0;
|
||||
});
|
||||
|
||||
expect(hasButtons).toBe(true);
|
||||
}, 120000);
|
||||
});
|
||||
|
||||
// ==================== CSS Selector Syntax Tests ====================
|
||||
describe('CSS Selector Syntax Validation', () => {
|
||||
it('should have valid CSS selector syntax for common selectors', async () => {
|
||||
// Test that selectors are syntactically valid by attempting to parse them in the browser
|
||||
const selectorsToValidate = [
|
||||
IRacingSelectorMap.common.mainModal,
|
||||
IRacingSelectorMap.common.checkoutButton,
|
||||
IRacingSelectorMap.common.wizardContainer,
|
||||
];
|
||||
|
||||
// Load a minimal page to test selector syntax
|
||||
await page.setContent('<html><body><div id="test"></div></body></html>');
|
||||
|
||||
for (const selector of selectorsToValidate) {
|
||||
// Verify selector is a valid string
|
||||
expect(typeof selector).toBe('string');
|
||||
expect(selector.length).toBeGreaterThan(0);
|
||||
|
||||
// Test selector syntax in the browser context (won't throw for valid CSS)
|
||||
const isValid = await page.evaluate((sel) => {
|
||||
try {
|
||||
document.querySelector(sel);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, selector);
|
||||
|
||||
expect(isValid, `Selector "${selector}" should be valid CSS`).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should have valid CSS selectors for each step', () => {
|
||||
for (let step = 1; step <= 18; step++) {
|
||||
const selectors = getStepSelectors(step);
|
||||
if (selectors?.container) {
|
||||
// Verify the selector string exists and is non-empty
|
||||
expect(typeof selectors.container).toBe('string');
|
||||
expect(selectors.container.length).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== Standalone Headless Verification ====================
|
||||
describe('E2E: Headless Mode Enforcement', () => {
|
||||
it('HEADLESS_MODE constant must be true', () => {
|
||||
// This test will fail if anyone changes HEADLESS_MODE to false
|
||||
expect(HEADLESS_MODE).toBe(true);
|
||||
});
|
||||
|
||||
it('should never allow headed mode configuration', () => {
|
||||
// Double-check that we're enforcing headless
|
||||
const isHeadless = HEADLESS_MODE === true;
|
||||
expect(isHeadless).toBe(true);
|
||||
|
||||
if (!isHeadless) {
|
||||
throw new Error('CRITICAL: Headed mode is forbidden! Set HEADLESS_MODE=true');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,390 +0,0 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
@@ -1,120 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import * as path from 'path';
|
||||
import { resolveFixturesPath } from '@/apps/companion/main/di-container';
|
||||
|
||||
describe('DIContainer path resolution', () => {
|
||||
describe('resolveFixturesPath', () => {
|
||||
describe('built mode (with /dist/ in path)', () => {
|
||||
it('should resolve fixtures path to monorepo root when dirname is apps/companion/dist/main', () => {
|
||||
// Given: The dirname as it would be in built Electron runtime (apps/companion/dist/main)
|
||||
const mockDirname = '/Users/test/Projects/gridpilot/apps/companion/dist/main';
|
||||
const relativePath = './resources/iracing-hosted-sessions';
|
||||
|
||||
// When: Resolving the fixtures path
|
||||
const resolved = resolveFixturesPath(relativePath, mockDirname);
|
||||
|
||||
// Then: Should resolve to monorepo root (4 levels up from apps/companion/dist/main)
|
||||
// Level 0: apps/companion/dist/main (__dirname)
|
||||
// Level 1: apps/companion/dist (../)
|
||||
// Level 2: apps/companion (../../)
|
||||
// Level 3: apps (../../../)
|
||||
// Level 4: gridpilot (monorepo root) (../../../../) ← CORRECT
|
||||
const expectedPath = '/Users/test/Projects/gridpilot/resources/iracing-hosted-sessions';
|
||||
expect(resolved).toBe(expectedPath);
|
||||
});
|
||||
|
||||
it('should navigate exactly 4 levels up in built mode', () => {
|
||||
// Given: A path with /dist/ that demonstrates the 4-level navigation
|
||||
const mockDirname = '/level4/level3/dist/level1';
|
||||
const relativePath = './target';
|
||||
|
||||
// When: Resolving the fixtures path
|
||||
const resolved = resolveFixturesPath(relativePath, mockDirname);
|
||||
|
||||
// Then: Should resolve to the path 4 levels up (root /)
|
||||
expect(resolved).toBe('/target');
|
||||
});
|
||||
|
||||
it('should work with different relative path formats in built mode', () => {
|
||||
// Given: Various relative path formats
|
||||
const mockDirname = '/Users/test/Projects/gridpilot/apps/companion/dist/main';
|
||||
|
||||
// When/Then: Different relative formats should all work
|
||||
expect(resolveFixturesPath('resources/fixtures', mockDirname))
|
||||
.toBe('/Users/test/Projects/gridpilot/resources/fixtures');
|
||||
|
||||
expect(resolveFixturesPath('./resources/fixtures', mockDirname))
|
||||
.toBe('/Users/test/Projects/gridpilot/resources/fixtures');
|
||||
});
|
||||
});
|
||||
|
||||
describe('dev mode (without /dist/ in path)', () => {
|
||||
it('should resolve fixtures path to monorepo root when dirname is apps/companion/main', () => {
|
||||
// Given: The dirname as it would be in dev mode (apps/companion/main)
|
||||
const mockDirname = '/Users/test/Projects/gridpilot/apps/companion/main';
|
||||
const relativePath = './resources/iracing-hosted-sessions';
|
||||
|
||||
// When: Resolving the fixtures path
|
||||
const resolved = resolveFixturesPath(relativePath, mockDirname);
|
||||
|
||||
// Then: Should resolve to monorepo root (3 levels up from apps/companion/main)
|
||||
// Level 0: apps/companion/main (__dirname)
|
||||
// Level 1: apps/companion (../)
|
||||
// Level 2: apps (../../)
|
||||
// Level 3: gridpilot (monorepo root) (../../../) ← CORRECT
|
||||
const expectedPath = '/Users/test/Projects/gridpilot/resources/iracing-hosted-sessions';
|
||||
expect(resolved).toBe(expectedPath);
|
||||
});
|
||||
|
||||
it('should navigate exactly 3 levels up in dev mode', () => {
|
||||
// Given: A path without /dist/ that demonstrates the 3-level navigation
|
||||
const mockDirname = '/level3/level2/level1';
|
||||
const relativePath = './target';
|
||||
|
||||
// When: Resolving the fixtures path
|
||||
const resolved = resolveFixturesPath(relativePath, mockDirname);
|
||||
|
||||
// Then: Should resolve to the path 3 levels up (root /)
|
||||
expect(resolved).toBe('/target');
|
||||
});
|
||||
|
||||
it('should work with different relative path formats in dev mode', () => {
|
||||
// Given: Various relative path formats
|
||||
const mockDirname = '/Users/test/Projects/gridpilot/apps/companion/main';
|
||||
|
||||
// When/Then: Different relative formats should all work
|
||||
expect(resolveFixturesPath('resources/fixtures', mockDirname))
|
||||
.toBe('/Users/test/Projects/gridpilot/resources/fixtures');
|
||||
|
||||
expect(resolveFixturesPath('./resources/fixtures', mockDirname))
|
||||
.toBe('/Users/test/Projects/gridpilot/resources/fixtures');
|
||||
});
|
||||
});
|
||||
|
||||
describe('absolute paths', () => {
|
||||
it('should return absolute paths unchanged in built mode', () => {
|
||||
// Given: An absolute path
|
||||
const absolutePath = '/some/absolute/path/to/fixtures';
|
||||
const mockDirname = '/Users/test/Projects/gridpilot/apps/companion/dist/main';
|
||||
|
||||
// When: Resolving an absolute path
|
||||
const resolved = resolveFixturesPath(absolutePath, mockDirname);
|
||||
|
||||
// Then: Should return the absolute path unchanged
|
||||
expect(resolved).toBe(absolutePath);
|
||||
});
|
||||
|
||||
it('should return absolute paths unchanged in dev mode', () => {
|
||||
// Given: An absolute path
|
||||
const absolutePath = '/some/absolute/path/to/fixtures';
|
||||
const mockDirname = '/Users/test/Projects/gridpilot/apps/companion/main';
|
||||
|
||||
// When: Resolving an absolute path
|
||||
const resolved = resolveFixturesPath(absolutePath, mockDirname);
|
||||
|
||||
// Then: Should return the absolute path unchanged
|
||||
expect(resolved).toBe(absolutePath);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,386 +0,0 @@
|
||||
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('<html></html>'),
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -181,7 +181,7 @@ describe('MockBrowserAutomationAdapter Integration Tests', () => {
|
||||
const result = await adapter.executeStep(stepId, config);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.stepId).toBe(1);
|
||||
expect(result.metadata?.stepId).toBe(1);
|
||||
});
|
||||
|
||||
it('should execute step 6 (modal step)', async () => {
|
||||
@@ -195,8 +195,8 @@ describe('MockBrowserAutomationAdapter Integration Tests', () => {
|
||||
const result = await adapter.executeStep(stepId, config);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.stepId).toBe(6);
|
||||
expect(result.wasModalStep).toBe(true);
|
||||
expect(result.metadata?.stepId).toBe(6);
|
||||
expect(result.metadata?.wasModalStep).toBe(true);
|
||||
});
|
||||
|
||||
it('should execute step 18 (final step)', async () => {
|
||||
@@ -210,8 +210,8 @@ describe('MockBrowserAutomationAdapter Integration Tests', () => {
|
||||
const result = await adapter.executeStep(stepId, config);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.stepId).toBe(18);
|
||||
expect(result.shouldStop).toBe(true);
|
||||
expect(result.metadata?.stepId).toBe(18);
|
||||
expect(result.metadata?.shouldStop).toBe(true);
|
||||
});
|
||||
|
||||
it('should simulate realistic step execution times', async () => {
|
||||
@@ -224,7 +224,7 @@ describe('MockBrowserAutomationAdapter Integration Tests', () => {
|
||||
|
||||
const result = await adapter.executeStep(stepId, config);
|
||||
|
||||
expect(result.executionTime).toBeGreaterThan(0);
|
||||
expect(result.metadata?.executionTime).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -274,9 +274,9 @@ describe('MockBrowserAutomationAdapter Integration Tests', () => {
|
||||
|
||||
const result = await adapter.executeStep(stepId, config);
|
||||
|
||||
expect(result.metrics).toBeDefined();
|
||||
expect(result.metrics.totalDelay).toBeGreaterThan(0);
|
||||
expect(result.metrics.operationCount).toBeGreaterThan(0);
|
||||
expect(result.metadata).toBeDefined();
|
||||
expect(result.metadata?.totalDelay).toBeGreaterThan(0);
|
||||
expect(result.metadata?.operationCount).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -16,15 +16,6 @@ describe('AutomationConfig', () => {
|
||||
|
||||
describe('getAutomationMode', () => {
|
||||
describe('NODE_ENV-based mode detection', () => {
|
||||
it('should return development mode when NODE_ENV=development', () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const mode = getAutomationMode();
|
||||
|
||||
expect(mode).toBe('development');
|
||||
});
|
||||
|
||||
it('should return production mode when NODE_ENV=production', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
@@ -43,33 +34,42 @@ describe('AutomationConfig', () => {
|
||||
expect(mode).toBe('test');
|
||||
});
|
||||
|
||||
it('should return development mode when NODE_ENV is not set', () => {
|
||||
it('should return test mode when NODE_ENV is not set', () => {
|
||||
delete process.env.NODE_ENV;
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const mode = getAutomationMode();
|
||||
|
||||
expect(mode).toBe('development');
|
||||
expect(mode).toBe('test');
|
||||
});
|
||||
|
||||
it('should return development mode for unknown NODE_ENV values', () => {
|
||||
it('should return test mode for unknown NODE_ENV values', () => {
|
||||
process.env.NODE_ENV = 'staging';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const mode = getAutomationMode();
|
||||
|
||||
expect(mode).toBe('development');
|
||||
expect(mode).toBe('test');
|
||||
});
|
||||
|
||||
it('should return test mode when NODE_ENV=development', () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const mode = getAutomationMode();
|
||||
|
||||
expect(mode).toBe('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy AUTOMATION_MODE support', () => {
|
||||
it('should map legacy dev mode to development with deprecation warning', () => {
|
||||
it('should map legacy dev mode to test with deprecation warning', () => {
|
||||
process.env.AUTOMATION_MODE = 'dev';
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
const mode = getAutomationMode();
|
||||
|
||||
expect(mode).toBe('development');
|
||||
expect(mode).toBe('test');
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('[DEPRECATED] AUTOMATION_MODE')
|
||||
);
|
||||
@@ -115,20 +115,13 @@ describe('AutomationConfig', () => {
|
||||
|
||||
describe('loadAutomationConfig', () => {
|
||||
describe('default configuration', () => {
|
||||
it('should return development mode when NODE_ENV is not set', () => {
|
||||
it('should return test mode when NODE_ENV is not set', () => {
|
||||
delete process.env.NODE_ENV;
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.mode).toBe('development');
|
||||
});
|
||||
|
||||
it('should return default devTools configuration', () => {
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.devTools?.debuggingPort).toBe(9222);
|
||||
expect(config.devTools?.browserWSEndpoint).toBeUndefined();
|
||||
expect(config.mode).toBe('test');
|
||||
});
|
||||
|
||||
it('should return default nutJs configuration', () => {
|
||||
@@ -146,44 +139,6 @@ describe('AutomationConfig', () => {
|
||||
expect(config.retryAttempts).toBe(3);
|
||||
expect(config.screenshotOnError).toBe(true);
|
||||
});
|
||||
|
||||
it('should return default fixture server configuration', () => {
|
||||
delete process.env.NODE_ENV;
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.fixtureServer?.port).toBe(3456);
|
||||
expect(config.fixtureServer?.autoStart).toBe(true);
|
||||
expect(config.fixtureServer?.fixturesPath).toBe('./resources/iracing-hosted-sessions');
|
||||
});
|
||||
});
|
||||
|
||||
describe('development mode configuration', () => {
|
||||
it('should return development mode when NODE_ENV=development', () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.mode).toBe('development');
|
||||
});
|
||||
|
||||
it('should parse CHROME_DEBUG_PORT', () => {
|
||||
process.env.CHROME_DEBUG_PORT = '9333';
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.devTools?.debuggingPort).toBe(9333);
|
||||
});
|
||||
|
||||
it('should read CHROME_WS_ENDPOINT', () => {
|
||||
process.env.CHROME_WS_ENDPOINT = 'ws://127.0.0.1:9222/devtools/browser/abc123';
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.devTools?.browserWSEndpoint).toBe('ws://127.0.0.1:9222/devtools/browser/abc123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('production mode configuration', () => {
|
||||
@@ -255,13 +210,11 @@ describe('AutomationConfig', () => {
|
||||
});
|
||||
|
||||
it('should fallback to defaults for invalid integer values', () => {
|
||||
process.env.CHROME_DEBUG_PORT = 'invalid';
|
||||
process.env.AUTOMATION_TIMEOUT = 'not-a-number';
|
||||
process.env.RETRY_ATTEMPTS = '';
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.devTools?.debuggingPort).toBe(9222);
|
||||
expect(config.defaultTimeout).toBe(30000);
|
||||
expect(config.retryAttempts).toBe(3);
|
||||
});
|
||||
@@ -274,109 +227,17 @@ describe('AutomationConfig', () => {
|
||||
expect(config.nutJs?.confidence).toBe(0.9);
|
||||
});
|
||||
|
||||
it('should fallback to development mode for invalid NODE_ENV', () => {
|
||||
it('should fallback to test mode for invalid NODE_ENV', () => {
|
||||
process.env.NODE_ENV = 'invalid-env';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.mode).toBe('development');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fixture server configuration', () => {
|
||||
it('should auto-start fixture server in development mode', () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.fixtureServer?.autoStart).toBe(true);
|
||||
});
|
||||
|
||||
it('should not auto-start fixture server in production mode', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.fixtureServer?.autoStart).toBe(false);
|
||||
});
|
||||
|
||||
it('should not auto-start fixture server in test mode', () => {
|
||||
process.env.NODE_ENV = 'test';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.fixtureServer?.autoStart).toBe(false);
|
||||
});
|
||||
|
||||
it('should parse FIXTURE_SERVER_PORT', () => {
|
||||
process.env.FIXTURE_SERVER_PORT = '4567';
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.fixtureServer?.port).toBe(4567);
|
||||
});
|
||||
|
||||
it('should parse FIXTURE_SERVER_PATH', () => {
|
||||
process.env.FIXTURE_SERVER_PATH = '/custom/fixtures';
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.fixtureServer?.fixturesPath).toBe('/custom/fixtures');
|
||||
});
|
||||
|
||||
it('should respect FIXTURE_SERVER_AUTO_START=false', () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
process.env.FIXTURE_SERVER_AUTO_START = 'false';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.fixtureServer?.autoStart).toBe(false);
|
||||
expect(config.mode).toBe('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('full configuration scenario', () => {
|
||||
it('should load complete development environment configuration', () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
process.env.CHROME_DEBUG_PORT = '9222';
|
||||
process.env.CHROME_WS_ENDPOINT = 'ws://localhost:9222/devtools/browser/test';
|
||||
process.env.AUTOMATION_TIMEOUT = '45000';
|
||||
process.env.RETRY_ATTEMPTS = '2';
|
||||
process.env.SCREENSHOT_ON_ERROR = 'true';
|
||||
process.env.FIXTURE_SERVER_PORT = '3456';
|
||||
process.env.FIXTURE_SERVER_PATH = './resources/iracing-hosted-sessions';
|
||||
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config).toEqual({
|
||||
mode: 'development',
|
||||
devTools: {
|
||||
debuggingPort: 9222,
|
||||
browserWSEndpoint: 'ws://localhost:9222/devtools/browser/test',
|
||||
},
|
||||
nutJs: {
|
||||
mouseSpeed: 1000,
|
||||
keyboardDelay: 50,
|
||||
windowTitle: 'iRacing',
|
||||
templatePath: './resources/templates',
|
||||
confidence: 0.9,
|
||||
},
|
||||
fixtureServer: {
|
||||
port: 3456,
|
||||
autoStart: true,
|
||||
fixturesPath: './resources/iracing-hosted-sessions',
|
||||
},
|
||||
defaultTimeout: 45000,
|
||||
retryAttempts: 2,
|
||||
screenshotOnError: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should load complete test environment configuration', () => {
|
||||
process.env.NODE_ENV = 'test';
|
||||
delete process.env.AUTOMATION_MODE;
|
||||
@@ -384,10 +245,7 @@ describe('AutomationConfig', () => {
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.mode).toBe('test');
|
||||
expect(config.devTools).toBeDefined();
|
||||
expect(config.nutJs).toBeDefined();
|
||||
expect(config.fixtureServer).toBeDefined();
|
||||
expect(config.fixtureServer?.autoStart).toBe(false);
|
||||
});
|
||||
|
||||
it('should load complete production environment configuration', () => {
|
||||
@@ -397,10 +255,7 @@ describe('AutomationConfig', () => {
|
||||
const config = loadAutomationConfig();
|
||||
|
||||
expect(config.mode).toBe('production');
|
||||
expect(config.devTools).toBeDefined();
|
||||
expect(config.nutJs).toBeDefined();
|
||||
expect(config.fixtureServer).toBeDefined();
|
||||
expect(config.fixtureServer?.autoStart).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as http from 'http';
|
||||
import * as path from 'path';
|
||||
import { FixtureServerService } from '@/packages/infrastructure/adapters/automation/FixtureServerService';
|
||||
|
||||
describe('FixtureServerService', () => {
|
||||
let service: FixtureServerService;
|
||||
let testPort: number;
|
||||
const fixturesPath = './resources/iracing-hosted-sessions';
|
||||
|
||||
beforeEach(() => {
|
||||
service = new FixtureServerService();
|
||||
testPort = 13400 + Math.floor(Math.random() * 100);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (service.isRunning()) {
|
||||
await service.stop();
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
});
|
||||
|
||||
describe('start', () => {
|
||||
it('should start the server on specified port', async () => {
|
||||
await service.start(testPort, fixturesPath);
|
||||
|
||||
expect(service.isRunning()).toBe(true);
|
||||
expect(service.getBaseUrl()).toBe(`http://localhost:${testPort}`);
|
||||
});
|
||||
|
||||
it('should throw error if server is already running', async () => {
|
||||
await service.start(testPort, fixturesPath);
|
||||
|
||||
await expect(service.start(testPort, fixturesPath)).rejects.toThrow(
|
||||
'Fixture server is already running'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if fixtures path does not exist', async () => {
|
||||
await expect(service.start(testPort, './non-existent-path')).rejects.toThrow(
|
||||
/Fixtures path does not exist/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stop', () => {
|
||||
it('should stop a running server', async () => {
|
||||
await service.start(testPort, fixturesPath);
|
||||
expect(service.isRunning()).toBe(true);
|
||||
|
||||
await service.stop();
|
||||
|
||||
expect(service.isRunning()).toBe(false);
|
||||
});
|
||||
|
||||
it('should resolve when server is not running', async () => {
|
||||
expect(service.isRunning()).toBe(false);
|
||||
|
||||
await expect(service.stop()).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('waitForReady', () => {
|
||||
it('should return true when server is ready', async () => {
|
||||
await service.start(testPort, fixturesPath);
|
||||
|
||||
const isReady = await service.waitForReady(5000);
|
||||
|
||||
expect(isReady).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when server is not running', async () => {
|
||||
const isReady = await service.waitForReady(500);
|
||||
|
||||
expect(isReady).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBaseUrl', () => {
|
||||
it('should return correct base URL with default port', () => {
|
||||
expect(service.getBaseUrl()).toBe('http://localhost:3456');
|
||||
});
|
||||
|
||||
it('should return correct base URL after starting on custom port', async () => {
|
||||
await service.start(testPort, fixturesPath);
|
||||
|
||||
expect(service.getBaseUrl()).toBe(`http://localhost:${testPort}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isRunning', () => {
|
||||
it('should return false when server is not started', () => {
|
||||
expect(service.isRunning()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when server is running', async () => {
|
||||
await service.start(testPort, fixturesPath);
|
||||
|
||||
expect(service.isRunning()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HTTP serving', () => {
|
||||
it('should serve HTML files from fixtures path', async () => {
|
||||
await service.start(testPort, fixturesPath);
|
||||
|
||||
const response = await makeRequest(`http://localhost:${testPort}/01-hosted-racing.html`);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.headers['content-type']).toBe('text/html');
|
||||
expect(response.body).toContain('<!DOCTYPE html');
|
||||
});
|
||||
|
||||
it('should serve health endpoint', async () => {
|
||||
await service.start(testPort, fixturesPath);
|
||||
|
||||
const response = await makeRequest(`http://localhost:${testPort}/health`);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.headers['content-type']).toBe('application/json');
|
||||
expect(JSON.parse(response.body)).toEqual({ status: 'ok' });
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent files', async () => {
|
||||
await service.start(testPort, fixturesPath);
|
||||
|
||||
const response = await makeRequest(`http://localhost:${testPort}/non-existent.html`);
|
||||
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('should include CORS headers', async () => {
|
||||
await service.start(testPort, fixturesPath);
|
||||
|
||||
const response = await makeRequest(`http://localhost:${testPort}/health`);
|
||||
|
||||
expect(response.headers['access-control-allow-origin']).toBe('*');
|
||||
});
|
||||
|
||||
it('should return 404 for path traversal attempts', async () => {
|
||||
await service.start(testPort, fixturesPath);
|
||||
|
||||
const response = await makeRequest(`http://localhost:${testPort}/../package.json`);
|
||||
|
||||
expect([403, 404]).toContain(response.statusCode);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
interface HttpResponse {
|
||||
statusCode: number;
|
||||
headers: Record<string, string>;
|
||||
body: string;
|
||||
}
|
||||
|
||||
function makeRequest(url: string): Promise<HttpResponse> {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(url, (res) => {
|
||||
let body = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
body += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
resolve({
|
||||
statusCode: res.statusCode || 0,
|
||||
headers: res.headers as Record<string, string>,
|
||||
body,
|
||||
});
|
||||
});
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user