remove companion tests

This commit is contained in:
2026-01-03 15:18:40 +01:00
parent 20f1b53c27
commit afbe42b0e1
67 changed files with 72 additions and 6325 deletions

View File

@@ -1,160 +0,0 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { StepId } from 'apps/companion/main/automation/domain/value-objects/StepId';
import {
FixtureServer,
PlaywrightAutomationAdapter,
} from 'core/automation/infrastructure//automation';
import { IRACING_SELECTORS } from 'core/automation/infrastructure//automation/dom/IRacingSelectors';
import { PinoLogAdapter } from 'core/automation/infrastructure//logging/PinoLogAdapter';
describe('Real Playwright hosted-session smoke (fixtures, steps 27)', () => {
let server: FixtureServer;
let adapter: PlaywrightAutomationAdapter;
let baseUrl: string;
beforeAll(async () => {
server = new FixtureServer();
const info = await server.start();
baseUrl = info.url;
const logger = new PinoLogAdapter();
adapter = new PlaywrightAutomationAdapter(
{
headless: true,
timeout: 8000,
mode: 'real',
baseUrl,
userDataDir: '',
},
logger,
);
const result = await adapter.connect(false);
expect(result.success).toBe(true);
expect(adapter.isConnected()).toBe(true);
expect(adapter.getPage()).not.toBeNull();
});
afterAll(async () => {
if (adapter) {
await adapter.disconnect();
}
if (server) {
await server.stop();
}
});
async function expectContextOpen(stepLabel: string) {
const page = adapter.getPage();
expect(page, `${stepLabel}: page should exist`).not.toBeNull();
const closed = await page!.isClosed();
expect(closed, `${stepLabel}: page should be open`).toBe(false);
expect(adapter.isConnected(), `${stepLabel}: adapter stays connected`).toBe(true);
}
async function navigateToFixtureStep(
stepNumber: number,
label: string,
stepKey?: keyof typeof IRACING_SELECTORS.wizard.stepContainers,
) {
const page = adapter.getPage();
expect(page).not.toBeNull();
await adapter.navigateToPage(server.getFixtureUrl(stepNumber));
await page!.waitForLoadState('domcontentloaded');
await expectContextOpen(`after navigate step ${stepNumber} (${label})`);
if (stepKey) {
const selector = IRACING_SELECTORS.wizard.stepContainers[stepKey];
const container = page!.locator(selector).first();
const count = await container.count();
expect(
count,
`${label}: expected container ${selector} to exist on fixture HTML`,
).toBeGreaterThan(0);
}
}
it(
'keeps browser context open and reaches Time Limits using real adapter against fixtures',
async () => {
await navigateToFixtureStep(2, 'Create a Race');
const step2Result = await adapter.executeStep(
StepId.create(2),
{} as Record<string, unknown>,
);
expect(step2Result.success).toBe(true);
await expectContextOpen('after step 2');
await navigateToFixtureStep(3, 'Race Information', 'raceInformation');
const step3Result = await adapter.executeStep(
StepId.create(3),
{
sessionName: 'GridPilot Smoke Session',
password: 'smokepw',
description: 'Real Playwright smoke path using fixtures',
} as Record<string, unknown>,
);
expect(step3Result.success).toBe(true);
await expectContextOpen('after step 3');
await navigateToFixtureStep(4, 'Server Details', 'serverDetails');
const step4Result = await adapter.executeStep(
StepId.create(4),
{
region: 'US',
startNow: true,
} as Record<string, unknown>,
);
expect(step4Result.success).toBe(true);
await expectContextOpen('after step 4');
await navigateToFixtureStep(5, 'Set Admins', 'admins');
const step5Result = await adapter.executeStep(
StepId.create(5),
{} as Record<string, unknown>,
);
expect(step5Result.success).toBe(true);
await expectContextOpen('after step 5');
await navigateToFixtureStep(6, 'Admins drawer', 'admins');
const step6Result = await adapter.executeStep(
StepId.create(6),
{
adminSearch: 'Marc',
} as Record<string, unknown>,
);
expect(step6Result.success).toBe(true);
await expectContextOpen('after step 6');
await navigateToFixtureStep(7, 'Time Limits', 'timeLimit');
const step7Result = await adapter.executeStep(
StepId.create(7),
{
practice: 10,
qualify: 10,
race: 20,
} as Record<string, unknown>,
);
expect(step7Result.success).toBe(true);
await expectContextOpen('after step 7');
const page = adapter.getPage();
expect(page).not.toBeNull();
const footerText = await page!.textContent('.wizard-footer');
expect(footerText || '').toMatch(/Cars/i);
const overlay = await page!.$('#gridpilot-overlay');
expect(overlay, 'overlay should be present in real mode').not.toBeNull();
},
60000,
);
});

View File

@@ -1,147 +0,0 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { DIContainer } from '../../../apps/companion/main/di-container';
import { StepId } from 'apps/companion/main/automation/domain/value-objects/StepId';
import type { HostedSessionConfig } from 'apps/companion/main/automation/domain/types/HostedSessionConfig';
import { PlaywrightAutomationAdapter } from 'core/automation/infrastructure//automation';
describe('Companion UI - hosted workflow via fixture-backed real stack', () => {
let container: DIContainer;
let adapter: PlaywrightAutomationAdapter;
let sessionId: string;
let originalEnv: string | undefined;
let originalFixtureFlag: string | undefined;
beforeAll(async () => {
originalEnv = process.env.NODE_ENV;
originalFixtureFlag = process.env.COMPANION_FIXTURE_HOSTED;
Object.defineProperty(process.env, 'NODE_ENV', {
value: 'test',
writable: true,
enumerable: true,
configurable: true
});
process.env.COMPANION_FIXTURE_HOSTED = '1';
DIContainer.resetInstance();
container = DIContainer.getInstance();
const connection = await container.initializeBrowserConnection();
expect(connection.success).toBe(true);
const browserAutomation = container.getBrowserAutomation();
expect(browserAutomation).toBeInstanceOf(PlaywrightAutomationAdapter);
adapter = browserAutomation as PlaywrightAutomationAdapter;
expect(adapter.isConnected()).toBe(true);
expect(adapter.getPage()).not.toBeNull();
}, 120000);
afterAll(async () => {
await container.shutdown();
Object.defineProperty(process.env, 'NODE_ENV', {
value: originalEnv,
writable: true,
enumerable: true,
configurable: true
});
process.env.COMPANION_FIXTURE_HOSTED = originalFixtureFlag;
});
async function waitForFinalSession(deadlineMs: number) {
const repo = container.getSessionRepository();
const deadline = Date.now() + deadlineMs;
let finalSession = null;
// eslint-disable-next-line no-constant-condition
while (true) {
const sessions = await repo.findAll();
finalSession = sessions[0] ?? null;
if (finalSession && (finalSession.state.isStoppedAtStep18() || finalSession.state.isCompleted())) {
break;
}
if (Date.now() > deadline) {
throw new Error('Timed out waiting for hosted workflow to complete via companion DI stack');
}
await new Promise((resolve) => setTimeout(resolve, 250));
}
return finalSession;
}
it(
'drives AutomationEngineAdapter via DI over fixtures and shows overlay progress',
async () => {
const startUseCase = container.getStartAutomationUseCase();
const repo = container.getSessionRepository();
const config: HostedSessionConfig = {
sessionName: 'Companion E2E - fixture hosted workflow',
serverName: 'Companion Fixture Server',
password: 'companion',
adminPassword: 'admin-companion',
maxDrivers: 20,
trackId: 'spa',
carIds: ['dallara-f3'],
weatherType: 'dynamic',
timeOfDay: 'afternoon',
sessionDuration: 60,
practiceLength: 10,
qualifyingLength: 10,
warmupLength: 5,
raceLength: 30,
startType: 'standing',
restarts: 'single-file',
damageModel: 'realistic',
trackState: 'auto'
};
const dto = await startUseCase.execute(config);
expect(dto.state).toBe('PENDING');
expect(dto.currentStep).toBe(1);
sessionId = dto.sessionId;
const session = await repo.findById(sessionId);
expect(session).not.toBeNull();
expect(session!.state.isPending()).toBe(true);
await adapter.navigateToPage('http://localhost:3456/');
const engine = container.getAutomationEngine();
await engine.executeStep(StepId.create(1), config);
const page = adapter.getPage();
expect(page).not.toBeNull();
await page!.waitForSelector('#gridpilot-overlay', { state: 'attached', timeout: 30000 });
const startingText = await page!.textContent('#gridpilot-action');
expect(startingText ?? '').not.toEqual('');
let reachedStep7OrBeyond = false;
const deadlineForProgress = Date.now() + 60000;
while (Date.now() < deadlineForProgress) {
const updated = await repo.findById(sessionId);
if (updated && updated.currentStep.value >= 7) {
reachedStep7OrBeyond = true;
break;
}
await new Promise((resolve) => setTimeout(resolve, 250));
}
expect(reachedStep7OrBeyond).toBe(true);
const overlayStepText = await page!.textContent('#gridpilot-step-text');
const overlayBody = (overlayStepText ?? '').trim().toLowerCase();
expect(overlayBody.length).toBeGreaterThan(0);
const finalSession = await waitForFinalSession(60000);
expect(finalSession.state.isStoppedAtStep18() || finalSession.state.isCompleted()).toBe(true);
expect(finalSession.errorMessage).toBeUndefined();
const progressState = finalSession.state.value;
expect(['STOPPED_AT_STEP_18', 'COMPLETED']).toContain(progressState);
},
180000
);
});

View File

@@ -1,173 +0,0 @@
Feature: Hosted Session Automation
As a league organizer using the GridPilot companion app
I want to automate the iRacing hosted session creation workflow
So that I can quickly set up race sessions without manual data entry
Background:
Given the companion app is running
And I am authenticated with iRacing
And I have a valid session configuration
Scenario: Complete 18-step automation workflow
Given I have a session configuration with:
| field | value |
| sessionName | League Race Week 1 |
| trackId | spa |
| carIds | dallara-f3 |
When I start the automation session
Then the session should be created with state "PENDING"
And the current step should be 1
When the automation progresses through all 18 steps
Then step 1 should navigate to "Hosted Racing"
And step 2 should click "Create a Race"
And step 3 should fill "Race Information"
And step 4 should configure "Server Details"
And step 5 should access "Set Admins"
And step 6 should handle "Add an Admin" modal
And step 7 should set "Time Limits"
And step 8 should access "Set Cars"
And step 9 should handle "Add a Car" modal
And step 10 should configure "Set Car Classes"
And step 11 should access "Set Track"
And step 12 should handle "Add a Track" modal
And step 13 should configure "Track Options"
And step 14 should set "Time of Day"
And step 15 should configure "Weather"
And step 16 should set "Race Options"
And step 17 should configure "Team Driving"
And step 18 should reach "Track Conditions"
And the session should stop at step 18
And the session state should be "STOPPED_AT_STEP_18"
And a manual submit warning should be displayed
Scenario: Modal step handling (step 6 - Add Admin)
Given I have started an automation session
And the automation has reached step 6
When the "Add an Admin" modal appears
Then the automation should detect the modal
And the automation should wait for modal content to load
And the automation should fill admin fields
And the automation should close the modal
And the automation should transition to step 7
Scenario: Modal step handling (step 9 - Add Car)
Given I have started an automation session
And the automation has reached step 9
When the "Add a Car" modal appears
Then the automation should detect the modal
And the automation should select the car "dallara-f3"
And the automation should confirm the selection
And the automation should close the modal
And the automation should transition to step 10
Scenario: Modal step handling (step 12 - Add Track)
Given I have started an automation session
And the automation has reached step 12
When the "Add a Track" modal appears
Then the automation should detect the modal
And the automation should select the track "spa"
And the automation should confirm the selection
And the automation should close the modal
And the automation should transition to step 13
Scenario: Safety checkpoint at step 18
Given I have started an automation session
And the automation has progressed to step 17
When the automation transitions to step 18
Then the automation should automatically stop
And the session state should be "STOPPED_AT_STEP_18"
And the current step should be 18
And no submit action should be executed
And a notification should inform the user to review before submitting
Scenario: Pause and resume automation
Given I have started an automation session
And the automation is at step 5
When I pause the automation
Then the session state should be "PAUSED"
And the current step should remain 5
When I resume the automation
Then the session state should be "IN_PROGRESS"
And the automation should continue from step 5
Scenario: Automation failure handling
Given I have started an automation session
And the automation is at step 8
When a browser automation error occurs
Then the session should transition to "FAILED" state
And an error message should be recorded
And the session should have a completedAt timestamp
And the user should be notified of the failure
Scenario: Invalid configuration rejection
Given I have a session configuration with:
| field | value |
| sessionName | |
| trackId | spa |
| carIds | dallara-f3|
When I attempt to start the automation session
Then the session creation should fail
And an error message should indicate "Session name cannot be empty"
And no session should be persisted
Scenario: Sequential step progression enforcement
Given I have started an automation session
And the automation is at step 5
When I attempt to skip directly to step 7
Then the transition should be rejected
And an error message should indicate "Cannot skip steps"
And the current step should remain 5
Scenario: Backward step prevention
Given I have started an automation session
And the automation has reached step 10
When I attempt to move back to step 9
Then the transition should be rejected
And an error message should indicate "Cannot move backward"
And the current step should remain 10
Scenario: Multiple car selection
Given I have a session configuration with:
| field | value |
| sessionName | Multi-class Race |
| trackId | spa |
| carIds | dallara-f3,porsche-911-gt3,bmw-m4-gt4 |
When I start the automation session
And the automation reaches step 9
Then all three cars should be added via the modal
And the automation should handle the modal three times
And the automation should transition to step 10
Scenario: Session state persistence
Given I have started an automation session
And the automation has reached step 12
When the application restarts
Then the session should be recoverable from storage
And the session state should be "IN_PROGRESS"
And the current step should be 12
And the session configuration should be intact
Scenario: Concurrent session prevention
Given I have started an automation session
And the session is in progress
When I attempt to start another automation session
Then the second session creation should be queued or rejected
And a warning should inform about the active session
Scenario: Elapsed time tracking
Given I have started an automation session
When the automation runs for 5 seconds
And I query the session status
Then the elapsed time should be approximately 5000 milliseconds
And the elapsed time should increase while in progress
Scenario: Complete workflow with realistic timings
Given I have a session configuration
When I start the automation session
Then each step should take between 200ms and 1000ms
And modal steps should take longer than regular steps
And the total workflow should complete in under 30 seconds
And the session should stop at step 18 without submitting

View File

@@ -1,146 +0,0 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { StepId } from 'apps/companion/main/automation/domain/value-objects/StepId';
import {
PlaywrightAutomationAdapter,
} from 'core/automation/infrastructure//automation';
import {
IRACING_SELECTORS,
IRACING_TIMEOUTS,
} from 'core/automation/infrastructure//automation/dom/IRacingSelectors';
import { PinoLogAdapter } from 'core/automation/infrastructure//logging/PinoLogAdapter';
const shouldRun = process.env.HOSTED_REAL_E2E === '1';
const describeMaybe = shouldRun ? describe : describe.skip;
describeMaybe('Real-site hosted session Cars flow (members.iracing.com)', () => {
let adapter: PlaywrightAutomationAdapter;
beforeAll(async () => {
const logger = new PinoLogAdapter();
adapter = new PlaywrightAutomationAdapter(
{
headless: true,
timeout: IRACING_TIMEOUTS.navigation,
mode: 'real',
baseUrl: '',
userDataDir: '',
},
logger,
);
const result = await adapter.connect(false);
expect(result.success).toBe(true);
expect(adapter.isConnected()).toBe(true);
const step1Result = await adapter.executeStep(StepId.create(1), {});
expect(step1Result.success).toBe(true);
const step2Result = await adapter.executeStep(StepId.create(2), {});
expect(step2Result.success).toBe(true);
const page = adapter.getPage();
expect(page).not.toBeNull();
const createRaceButton = page!
.locator(IRACING_SELECTORS.hostedRacing.createRaceButton)
.first();
await expect(
createRaceButton.count(),
'Create Race button should exist on Hosted Racing page',
).resolves.toBeGreaterThan(0);
await createRaceButton.click({ timeout: IRACING_TIMEOUTS.elementWait });
const raceInfoContainer = page!
.locator(IRACING_SELECTORS.wizard.stepContainers.raceInformation)
.first();
await raceInfoContainer.waitFor({
state: 'attached',
timeout: IRACING_TIMEOUTS.elementWait,
});
expect(await raceInfoContainer.count()).toBeGreaterThan(0);
const sessionConfig = {
sessionName: 'GridPilot Real Cars flow',
password: 'cars-flow-secret',
description: 'Real-site cars flow short path',
};
const step3Result = await adapter.executeStep(StepId.create(3), sessionConfig);
expect(step3Result.success).toBe(true);
const carsSidebarLink = page!
.locator(IRACING_SELECTORS.wizard.sidebarLinks.cars)
.first();
await carsSidebarLink.waitFor({
state: 'attached',
timeout: IRACING_TIMEOUTS.elementWait,
});
await carsSidebarLink.click({ timeout: IRACING_TIMEOUTS.elementWait });
const carsContainer = page!
.locator(IRACING_SELECTORS.wizard.stepContainers.cars)
.first();
await carsContainer.waitFor({
state: 'attached',
timeout: IRACING_TIMEOUTS.elementWait,
});
expect(await carsContainer.count()).toBeGreaterThan(0);
}, 300_000);
afterAll(async () => {
if (adapter) {
await adapter.disconnect();
}
});
it(
'opens Add Car UI on real site and lists at least one car',
async () => {
const page = adapter.getPage();
expect(page).not.toBeNull();
const carsContainer = page!
.locator(IRACING_SELECTORS.wizard.stepContainers.cars)
.first();
await carsContainer.waitFor({
state: 'attached',
timeout: IRACING_TIMEOUTS.elementWait,
});
expect(await carsContainer.count()).toBeGreaterThan(0);
const addCarButton = page!
.locator(IRACING_SELECTORS.steps.addCarButton)
.first();
await addCarButton.waitFor({
state: 'attached',
timeout: IRACING_TIMEOUTS.elementWait,
});
expect(await addCarButton.count()).toBeGreaterThan(0);
await addCarButton.click({ timeout: IRACING_TIMEOUTS.elementWait });
const addCarModal = page!
.locator(IRACING_SELECTORS.steps.addCarModal)
.first();
await addCarModal.waitFor({
state: 'attached',
timeout: IRACING_TIMEOUTS.elementWait,
});
expect(await addCarModal.count()).toBeGreaterThan(0);
const carsTable = addCarModal
.locator('table.table.table-striped tbody tr')
.first();
await carsTable.waitFor({
state: 'attached',
timeout: IRACING_TIMEOUTS.elementWait,
});
const rowCount = await addCarModal
.locator('table.table.table-striped tbody tr')
.count();
expect(rowCount).toBeGreaterThan(0);
},
300_000,
);
});

View File

@@ -1,159 +0,0 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { StepId } from 'apps/companion/main/automation/domain/value-objects/StepId';
import {
PlaywrightAutomationAdapter,
} from 'core/automation/infrastructure//automation';
import {
IRACING_SELECTORS,
IRACING_TIMEOUTS,
} from 'core/automation/infrastructure//automation/dom/IRacingSelectors';
import { PinoLogAdapter } from 'core/automation/infrastructure//logging/PinoLogAdapter';
const shouldRun = process.env.HOSTED_REAL_E2E === '1';
const describeMaybe = shouldRun ? describe : describe.skip;
describeMaybe('Real-site hosted session smoke login and wizard entry (members.iracing.com)', () => {
let adapter: PlaywrightAutomationAdapter;
beforeAll(async () => {
const logger = new PinoLogAdapter();
adapter = new PlaywrightAutomationAdapter(
{
headless: true,
timeout: IRACING_TIMEOUTS.navigation,
mode: 'real',
baseUrl: '',
userDataDir: '',
},
logger,
);
const result = await adapter.connect(false);
expect(result.success).toBe(true);
expect(adapter.isConnected()).toBe(true);
}, 180_000);
afterAll(async () => {
if (adapter) {
await adapter.disconnect();
}
});
it(
'logs in, reaches Hosted Racing, and opens Create Race wizard',
async () => {
const step1Result = await adapter.executeStep(StepId.create(1), {});
expect(step1Result.success).toBe(true);
const page = adapter.getPage();
expect(page).not.toBeNull();
const createRaceButton = page!
.locator(IRACING_SELECTORS.hostedRacing.createRaceButton)
.first();
await expect(
createRaceButton.count(),
'Create Race button should exist on Hosted Racing page',
).resolves.toBeGreaterThan(0);
const hostedTab = page!
.locator(IRACING_SELECTORS.hostedRacing.hostedTab)
.first();
await hostedTab.waitFor({
state: 'attached',
timeout: IRACING_TIMEOUTS.elementWait,
});
const step2Result = await adapter.executeStep(StepId.create(2), {});
expect(step2Result.success).toBe(true);
const modalSelector = IRACING_SELECTORS.hostedRacing.createRaceModal;
const modal = page!.locator(modalSelector).first();
await modal.waitFor({
state: 'attached',
timeout: IRACING_TIMEOUTS.elementWait,
});
const newRaceButton = page!
.locator(IRACING_SELECTORS.hostedRacing.newRaceButton)
.first();
await newRaceButton.waitFor({
state: 'attached',
timeout: IRACING_TIMEOUTS.elementWait,
});
await newRaceButton.click({ timeout: IRACING_TIMEOUTS.elementWait });
const raceInfoContainer = page!
.locator(IRACING_SELECTORS.wizard.stepContainers.raceInformation)
.first();
await raceInfoContainer.waitFor({
state: 'attached',
timeout: IRACING_TIMEOUTS.elementWait,
});
const modalContent = await page!
.locator(IRACING_SELECTORS.wizard.modalContent)
.first()
.count();
expect(
modalContent,
'Race creation wizard modal content should be present',
).toBeGreaterThan(0);
},
300_000,
);
it(
'detects login guard and does not attempt Create a Race when not authenticated',
async () => {
const step1Result = await adapter.executeStep(StepId.create(1), {});
expect(step1Result.success).toBe(true);
const page = adapter.getPage();
expect(page).not.toBeNull();
const currentUrl = page!.url();
expect(currentUrl).not.toEqual('about:blank');
expect(currentUrl.toLowerCase()).toContain('iracing');
expect(currentUrl.toLowerCase()).toSatisfy((u: string) =>
u.includes('oauth.iracing.com') ||
u.includes('members.iracing.com') ||
u.includes('/login'),
);
const emailInput = page!
.locator(IRACING_SELECTORS.login.emailInput)
.first();
const passwordInput = page!
.locator(IRACING_SELECTORS.login.passwordInput)
.first();
const hasEmail = (await emailInput.count()) > 0;
const hasPassword = (await passwordInput.count()) > 0;
if (!hasEmail && !hasPassword) {
return;
}
await emailInput.waitFor({
state: 'visible',
timeout: IRACING_TIMEOUTS.elementWait,
});
await passwordInput.waitFor({
state: 'visible',
timeout: IRACING_TIMEOUTS.elementWait,
});
const createRaceButton = page!
.locator(IRACING_SELECTORS.hostedRacing.createRaceButton)
.first();
const createRaceCount = await createRaceButton.count();
expect(createRaceCount).toBe(0);
},
300_000,
);
});

View File

@@ -1,162 +0,0 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { promises as fs } from 'fs';
import path from 'path';
import { StepId } from 'apps/companion/main/automation/domain/value-objects/StepId';
import {
PlaywrightAutomationAdapter,
} from 'core/automation/infrastructure//automation';
import {
IRACING_SELECTORS,
IRACING_TIMEOUTS,
} from 'core/automation/infrastructure//automation/dom/IRacingSelectors';
import { PinoLogAdapter } from 'core/automation/infrastructure//logging/PinoLogAdapter';
const shouldRun = process.env.HOSTED_REAL_E2E === '1';
const describeMaybe = shouldRun ? describe : describe.skip;
describeMaybe('Real-site hosted session Race Information step (members.iracing.com)', () => {
let adapter: PlaywrightAutomationAdapter;
beforeAll(async () => {
const logger = new PinoLogAdapter();
adapter = new PlaywrightAutomationAdapter(
{
headless: true,
timeout: IRACING_TIMEOUTS.navigation,
mode: 'real',
baseUrl: '',
userDataDir: '',
},
logger,
);
const result = await adapter.connect(false);
expect(result.success).toBe(true);
expect(adapter.isConnected()).toBe(true);
const step1Result = await adapter.executeStep(StepId.create(1), {});
expect(step1Result.success).toBe(true);
const step2Result = await adapter.executeStep(StepId.create(2), {});
expect(step2Result.success).toBe(true);
const page = adapter.getPage();
expect(page).not.toBeNull();
const createRaceButton = page!
.locator(IRACING_SELECTORS.hostedRacing.createRaceButton)
.first();
await expect(
createRaceButton.count(),
'Create Race button should exist on Hosted Racing page',
).resolves.toBeGreaterThan(0);
await createRaceButton.click({ timeout: IRACING_TIMEOUTS.elementWait });
const raceInfoContainer = page!
.locator(IRACING_SELECTORS.wizard.stepContainers.raceInformation)
.first();
await raceInfoContainer.waitFor({
state: 'attached',
timeout: IRACING_TIMEOUTS.elementWait,
});
expect(await raceInfoContainer.count()).toBeGreaterThan(0);
}, 300_000);
afterAll(async () => {
if (adapter) {
await adapter.disconnect();
}
});
it(
'shows Race Information sidebar text matching fixtures and keeps text inputs writable',
async () => {
const page = adapter.getPage();
expect(page).not.toBeNull();
const sidebarLink = page!
.locator(IRACING_SELECTORS.wizard.sidebarLinks.raceInformation)
.first();
await sidebarLink.waitFor({
state: 'attached',
timeout: IRACING_TIMEOUTS.elementWait,
});
const sidebarText = (await sidebarLink.innerText()).trim();
expect(sidebarText.length).toBeGreaterThan(0);
let fixtureSidebarText: string | null = null;
try {
const fixturePath = path.join(
process.cwd(),
'html-dumps-optimized',
'iracing-hosted-sessions',
'03-race-information.json',
);
const raw = await fs.readFile(fixturePath, 'utf8');
const items = JSON.parse(raw) as Array<{ i: string; t: string }>;
const sidebarItem =
items.find(
(i) =>
i.i === 'wizard-sidebar-link-set-session-information' &&
typeof i.t === 'string',
) ?? null;
if (sidebarItem) {
fixtureSidebarText = sidebarItem.t;
}
} catch {
fixtureSidebarText = null;
}
if (fixtureSidebarText) {
const expected = fixtureSidebarText.toLowerCase();
const actual = sidebarText.toLowerCase();
expect(
actual.includes('race') || actual.includes(expected.slice(0, 4)),
).toBe(true);
}
const config = {
sessionName: 'GridPilot Real Race Information',
password: 'real-site-secret',
description: 'Real-site Race Information writable fields check',
};
const result = await adapter.executeStep(StepId.create(3), config);
expect(result.success).toBe(true);
const sessionNameInput = page!
.locator(IRACING_SELECTORS.steps.sessionName)
.first();
const passwordInput = page!
.locator(IRACING_SELECTORS.steps.password)
.first();
const descriptionInput = page!
.locator(IRACING_SELECTORS.steps.description)
.first();
await sessionNameInput.waitFor({
state: 'attached',
timeout: IRACING_TIMEOUTS.elementWait,
});
await passwordInput.waitFor({
state: 'attached',
timeout: IRACING_TIMEOUTS.elementWait,
});
await descriptionInput.waitFor({
state: 'attached',
timeout: IRACING_TIMEOUTS.elementWait,
});
const sessionNameValue = await sessionNameInput.inputValue();
const passwordValue = await passwordInput.inputValue();
const descriptionValue = await descriptionInput.inputValue();
expect(sessionNameValue).toBe(config.sessionName);
expect(passwordValue).toBe(config.password);
expect(descriptionValue).toBe(config.description);
},
300_000,
);
});

View File

@@ -1,8 +0,0 @@
/**
* Legacy Cucumber step definitions for real iRacing automation.
*
* Native OS-level automation and these steps have been retired.
* This file is excluded from TypeScript builds and is kept only as
* historical documentation. No executable step definitions remain.
*/
export {};

View File

@@ -1,30 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
describe('Step 1 hosted racing', () => {
let harness: StepHarness;
beforeEach(async () => {
harness = await createStepHarness();
});
afterEach(async () => {
await harness.dispose();
});
it('executes on Hosted Racing page in mock wizard', async () => {
await harness.navigateToFixtureStep(1);
const page = harness.adapter.getPage();
expect(page).not.toBeNull();
const bodyText = await page!.textContent('body');
expect(bodyText).toContain('Create a Race');
const result = await harness.executeStep(1, {});
expect(result.success).toBe(true);
expect(result.error).toBeUndefined();
});
});

View File

@@ -1,50 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
import { IRACING_SELECTORS } from 'core/automation/infrastructure//automation/dom/IRacingSelectors';
describe('Step 2 create race', () => {
let harness: StepHarness;
beforeEach(async () => {
harness = await createStepHarness();
});
afterEach(async () => {
await harness.dispose();
});
it('opens the real Create Race confirmation modal with Last Settings / New Race options', async () => {
await harness.navigateToFixtureStep(2);
const page = harness.adapter.getPage();
expect(page).not.toBeNull();
const bodyTextBefore = await page!.textContent('body');
expect(bodyTextBefore).toContain('Create a Race');
const result = await harness.executeStep(2, {});
expect(result.success).toBe(true);
expect(result.error).toBeUndefined();
await page!.waitForSelector(
IRACING_SELECTORS.hostedRacing.createRaceModal,
);
const modalText = await page!.textContent(
IRACING_SELECTORS.hostedRacing.createRaceModal,
);
expect(modalText).toMatch(/Last Settings/i);
expect(modalText).toMatch(/New Race/i);
const lastSettingsButton = await page!.$(
IRACING_SELECTORS.hostedRacing.lastSettingsButton,
);
const newRaceButton = await page!.$(
IRACING_SELECTORS.hostedRacing.newRaceButton,
);
expect(lastSettingsButton).not.toBeNull();
expect(newRaceButton).not.toBeNull();
});
});

View File

@@ -1,61 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
import { IRACING_SELECTORS } from 'core/automation/infrastructure//automation/dom/IRacingSelectors';
describe('Step 3 race information', () => {
let harness: StepHarness;
beforeEach(async () => {
harness = await createStepHarness();
});
afterEach(async () => {
await harness.dispose();
});
it('fills race information on Race Information page and persists values in form fields', async () => {
await harness.navigateToFixtureStep(3);
const page = harness.adapter.getPage();
expect(page).not.toBeNull();
const sidebarRaceInfo = await page!
.locator(IRACING_SELECTORS.wizard.sidebarLinks.raceInformation)
.first()
.innerText();
expect(sidebarRaceInfo).toMatch(/Race Information/i);
const config = {
sessionName: 'GridPilot E2E Session',
password: 'secret',
description: 'Step 3 race information E2E',
};
const result = await harness.executeStep(3, config);
expect(result.success).toBe(true);
expect(result.error).toBeUndefined();
const sessionNameInput = page!
.locator(IRACING_SELECTORS.steps.sessionName)
.first();
const passwordInput = page!
.locator(IRACING_SELECTORS.steps.password)
.first();
const descriptionInput = page!
.locator(IRACING_SELECTORS.steps.description)
.first();
const sessionNameValue = await sessionNameInput.inputValue();
const passwordValue = await passwordInput.inputValue();
const descriptionValue = await descriptionInput.inputValue();
expect(sessionNameValue).toBe(config.sessionName);
expect(passwordValue).toBe(config.password);
expect(descriptionValue).toBe(config.description);
const footerText = await page!.textContent('.wizard-footer');
expect(footerText).toMatch(/Server Details|Admins/i);
});
});

View File

@@ -1,61 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
import { IRACING_SELECTORS } from 'core/automation/infrastructure//automation/dom/IRacingSelectors';
describe('Step 4 server details', () => {
let harness: StepHarness;
beforeEach(async () => {
harness = await createStepHarness();
});
afterEach(async () => {
await harness.dispose();
});
it('executes on Server Details page, applies region/start toggle, and progresses toward Admins', async () => {
await harness.navigateToFixtureStep(4);
const page = harness.adapter.getPage();
expect(page).not.toBeNull();
const sidebarServerDetails = await page!
.locator(IRACING_SELECTORS.wizard.sidebarLinks.serverDetails)
.first()
.innerText();
expect(sidebarServerDetails).toMatch(/Server Details/i);
const serverDetailsContainer = page!
.locator(IRACING_SELECTORS.wizard.stepContainers.serverDetails)
.first();
expect(await serverDetailsContainer.count()).toBeGreaterThan(0);
const config = {
region: 'US-East-OH',
startNow: true,
};
const result = await harness.executeStep(4, config);
expect(result.success).toBe(true);
expect(result.error).toBeUndefined();
const currentServerHeader = await page!
.locator('#set-server-details button:has-text("Current Server")')
.first()
.innerText();
expect(currentServerHeader.toLowerCase()).toContain('us-east');
const startToggle = page!
.locator(IRACING_SELECTORS.steps.startNow)
.first();
const startNowChecked =
(await startToggle.getAttribute('checked')) !== null ||
(await startToggle.getAttribute('aria-checked')) === 'true';
expect(startNowChecked).toBe(true);
const footerText = await page!.textContent('.wizard-footer');
expect(footerText).toMatch(/Admins/i);
});
});

View File

@@ -1,51 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
import { IRACING_SELECTORS } from 'core/automation/infrastructure//automation/dom/IRacingSelectors';
describe('Step 5 set admins', () => {
let harness: StepHarness;
beforeEach(async () => {
harness = await createStepHarness();
});
afterEach(async () => {
await harness.dispose();
});
it('executes on Set Admins page and leaves at least one admin in the selected admins table when progressing to Time Limit', async () => {
await harness.navigateToFixtureStep(5);
const page = harness.adapter.getPage();
expect(page).not.toBeNull();
const sidebarAdmins = await page!
.locator(IRACING_SELECTORS.wizard.sidebarLinks.admins)
.first()
.innerText();
expect(sidebarAdmins).toMatch(/Admins/i);
const adminsContainer = page!
.locator(IRACING_SELECTORS.wizard.stepContainers.admins)
.first();
expect(await adminsContainer.count()).toBeGreaterThan(0);
const bodyText = await page!.textContent('body');
expect(bodyText).toContain('Add an Admin');
const result = await harness.executeStep(5, {});
expect(result.success).toBe(true);
expect(result.error).toBeUndefined();
const selectedAdminsText =
(await page!.textContent(
'#set-admins tbody[data-testid="admin-display-name-list"]',
)) ?? '';
expect(selectedAdminsText.trim()).not.toEqual('');
const footerText = await page!.textContent('.wizard-footer');
expect(footerText).toContain('Time Limit');
});
});

View File

@@ -1,77 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
import { IRACING_SELECTORS } from 'core/automation/infrastructure//automation/dom/IRacingSelectors';
describe('Step 6 admins', () => {
let harness: StepHarness;
beforeEach(async () => {
harness = await createStepHarness();
});
afterEach(async () => {
await harness.dispose();
});
it('completes successfully from Set Admins page and leaves selected admins populated', async () => {
await harness.navigateToFixtureStep(5);
const page = harness.adapter.getPage();
expect(page).not.toBeNull();
const sidebarAdmins = await page!
.locator(IRACING_SELECTORS.wizard.sidebarLinks.admins)
.first()
.innerText();
expect(sidebarAdmins).toMatch(/Admins/i);
const adminsContainer = page!
.locator(IRACING_SELECTORS.wizard.stepContainers.admins)
.first();
expect(await adminsContainer.count()).toBeGreaterThan(0);
const result = await harness.executeStep(6, {
adminSearch: 'Marc',
});
expect(result.success).toBe(true);
const selectedAdminsText =
(await page!.textContent(
'#set-admins tbody[data-testid="admin-display-name-list"]',
)) ?? '';
expect(selectedAdminsText.trim()).not.toEqual('');
const footerText = await page!.textContent('.wizard-footer');
expect(footerText).toContain('Time Limit');
});
it('handles Add Admin drawer state without regression and preserves selected admins list', async () => {
await harness.navigateToFixtureStep(6);
const page = harness.adapter.getPage();
expect(page).not.toBeNull();
const adminsContainer = page!
.locator(IRACING_SELECTORS.wizard.stepContainers.admins)
.first();
expect(await adminsContainer.count()).toBeGreaterThan(0);
const header = await page!.textContent('#set-admins .card-header');
expect(header).toContain('Set Admins');
const result = await harness.executeStep(6, {
adminSearch: 'Mintel',
});
expect(result.success).toBe(true);
const selectedAdminsText =
(await page!.textContent(
'#set-admins tbody[data-testid="admin-display-name-list"]',
)) ?? '';
expect(selectedAdminsText.trim()).not.toEqual('');
const footerText = await page!.textContent('.wizard-footer');
expect(footerText).toContain('Time Limit');
});
});

View File

@@ -1,49 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
import { IRACING_SELECTORS } from 'core/automation/infrastructure//automation/dom/IRacingSelectors';
describe('Step 7 time limits', () => {
let harness: StepHarness;
beforeEach(async () => {
harness = await createStepHarness();
});
afterEach(async () => {
await harness.dispose();
});
it('executes on Time Limits page, applies sliders, and navigates to Cars', async () => {
await harness.navigateToFixtureStep(7);
const page = harness.adapter.getPage();
expect(page).not.toBeNull();
const timeLimitContainer = page!
.locator(IRACING_SELECTORS.wizard.stepContainers.timeLimit)
.first();
expect(await timeLimitContainer.count()).toBeGreaterThan(0);
const result = await harness.executeStep(7, {
practice: 10,
qualify: 10,
race: 20,
});
expect(result.success).toBe(true);
expect(result.error).toBeUndefined();
const raceSlider = page!
.locator(IRACING_SELECTORS.steps.race)
.first();
const raceSliderExists = await raceSlider.count();
expect(raceSliderExists).toBeGreaterThan(0);
const raceValueAttr =
(await raceSlider.getAttribute('data-value')) ??
(await raceSlider.inputValue().catch(() => null));
expect(raceValueAttr).toBe('20');
const footerText = await page!.textContent('.wizard-footer');
expect(footerText).toMatch(/Cars/i);
});
});

View File

@@ -1,70 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
import { IRACING_SELECTORS } from 'core/automation/infrastructure//automation/dom/IRacingSelectors';
describe('Step 8 cars', () => {
let harness: StepHarness;
beforeEach(async () => {
harness = await createStepHarness();
});
afterEach(async () => {
await harness.dispose();
});
describe('alignment', () => {
it('executes on Cars page in mock wizard and exposes Add Car UI', async () => {
await harness.navigateToFixtureStep(8);
const page = harness.adapter.getPage();
expect(page).not.toBeNull();
const carsContainer = page!
.locator(IRACING_SELECTORS.wizard.stepContainers.cars)
.first();
expect(await carsContainer.count()).toBeGreaterThan(0);
const addCarButton = page!
.locator(IRACING_SELECTORS.steps.addCarButton)
.first();
const addCarText = await addCarButton.innerText();
expect(addCarText.toLowerCase()).toContain('add a car');
const result = await harness.executeStepWithFixtureMismatch(8, {});
expect(result.success).toBe(true);
expect(result.error).toBeUndefined();
});
});
describe('state validation', () => {
it('fails validation when executed on Track page instead of Cars page', async () => {
await harness.navigateToFixtureStep(11);
await harness.adapter.getPage()?.waitForLoadState('domcontentloaded');
await expect(async () => {
await harness.executeStepWithFixtureMismatch(8, {});
}).rejects.toThrow(/Step 8 FAILED validation/i);
});
it('fails fast on Step 8 if already past Cars page', async () => {
await harness.navigateToFixtureStep(11);
await harness.adapter.getPage()?.waitForLoadState('domcontentloaded');
await expect(async () => {
await harness.executeStepWithFixtureMismatch(8, {});
}).rejects.toThrow(/Step 8 FAILED validation/i);
});
it('passes validation when on Cars page', async () => {
await harness.navigateToFixtureStep(8);
await harness.adapter.getPage()?.waitForLoadState('domcontentloaded');
const result = await harness.executeStepWithFixtureMismatch(8, {});
expect(result.success).toBe(true);
});
});
});

View File

@@ -1,154 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
describe('Step 9 add car', () => {
let harness: StepHarness;
beforeEach(async () => {
harness = await createStepHarness();
});
afterEach(async () => {
await harness.dispose();
});
describe('happy path', () => {
it('adds a real car using the JSON-backed car list on Cars page', async () => {
await harness.navigateToFixtureStep(8);
await harness.adapter.getPage()?.waitForLoadState('domcontentloaded');
const page = harness.adapter.getPage();
expect(page).not.toBeNull();
const result = await harness.executeStepWithFixtureMismatch(9, {
carSearch: 'Acura ARX-06',
});
expect(result.success).toBe(true);
expect(result.error).toBeUndefined();
const carsTable = page!
.locator('#select-car-set-cars table.table.table-striped')
.first();
expect(await carsTable.count()).toBeGreaterThan(0);
const acuraCell = carsTable.locator('tbody tr td >> text=Acura ARX-06 GTP');
expect(await acuraCell.count()).toBeGreaterThan(0);
});
});
describe('state validation', () => {
it('throws when executed on Track page instead of Cars page', async () => {
await harness.navigateToFixtureStep(11);
await harness.adapter.getPage()?.waitForLoadState('domcontentloaded');
await expect(async () => {
await harness.executeStepWithFixtureMismatch(9, {
carSearch: 'Mazda MX-5',
});
}).rejects.toThrow(/Step 9 FAILED validation/i);
});
it('detects state mismatch when Cars button is missing', async () => {
await harness.navigateToFixtureStep(11);
await harness.adapter.getPage()?.waitForLoadState('domcontentloaded');
await expect(async () => {
await harness.executeStepWithFixtureMismatch(9, {
carSearch: 'Porsche 911',
});
}).rejects.toThrow(/Step 9 FAILED validation/i);
});
it('detects when Track container is present instead of Cars page', async () => {
await harness.navigateToFixtureStep(11);
await harness.adapter.getPage()?.waitForLoadState('domcontentloaded');
await expect(async () => {
await harness.executeStepWithFixtureMismatch(9, {
carSearch: 'Ferrari 488',
});
}).rejects.toThrow(/Step 9 FAILED validation/i);
});
it('passes validation when on Cars page', async () => {
await harness.navigateToFixtureStep(8);
await harness.adapter.getPage()?.waitForLoadState('domcontentloaded');
const result = await harness.executeStepWithFixtureMismatch(9, {
carSearch: 'Acura ARX-06',
});
expect(result.success).toBe(true);
const page = harness.adapter.getPage();
expect(page).not.toBeNull();
const carsTable = page!
.locator('#select-car-set-cars table.table.table-striped')
.first();
expect(await carsTable.count()).toBeGreaterThan(0);
const acuraCell = carsTable.locator('tbody tr td >> text=Acura ARX-06 GTP');
expect(await acuraCell.count()).toBeGreaterThan(0);
});
it('provides detailed error context in validation failure', async () => {
await harness.navigateToFixtureStep(11);
await harness.adapter.getPage()?.waitForLoadState('domcontentloaded');
let errorMessage = '';
try {
await harness.executeStepWithFixtureMismatch(9, {
carSearch: 'BMW M4',
});
} catch (error) {
errorMessage = error instanceof Error ? error.message : String(error);
}
expect(errorMessage).toContain('Step 9');
expect(errorMessage).toMatch(/validation|mismatch|wrong page/i);
});
it('validates page state before attempting any Step 9 actions', async () => {
await harness.navigateToFixtureStep(11);
await harness.adapter.getPage()?.waitForLoadState('domcontentloaded');
const page = harness.adapter.getPage();
if (!page) {
throw new Error('Page not available');
}
let carModalOpened = false;
page.on('framenavigated', () => {
carModalOpened = true;
});
let validationError = false;
try {
await harness.executeStepWithFixtureMismatch(9, {
carSearch: 'Audi R8',
});
} catch {
validationError = true;
}
expect(validationError).toBe(true);
expect(carModalOpened).toBe(false);
});
it('checks wizard footer state in Step 9', async () => {
await harness.navigateToFixtureStep(11);
await harness.adapter.getPage()?.waitForLoadState('domcontentloaded');
await expect(async () => {
await harness.executeStepWithFixtureMismatch(9, {
carSearch: 'McLaren 720S',
});
}).rejects.toThrow();
});
});
});

View File

@@ -1,33 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
describe('Step 10 car classes', () => {
let harness: StepHarness;
beforeEach(async () => {
harness = await createStepHarness();
});
afterEach(async () => {
await harness.dispose();
});
it('executes on Car Classes page and keeps wizard on Track path', async () => {
await harness.navigateToFixtureStep(10);
const page = harness.adapter.getPage();
expect(page).not.toBeNull();
const bodyText = await page!.textContent('body');
expect(bodyText).toContain('Add a Car Class');
const result = await harness.executeStep(10, {});
expect(result.success).toBe(true);
expect(result.error).toBeUndefined();
const footerText = await page!.textContent('.wizard-footer');
expect(footerText).toMatch(/Track/i);
});
});

View File

@@ -1,35 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
describe('Step 11 track', () => {
describe('state validation', () => {
let harness: StepHarness;
beforeEach(async () => {
harness = await createStepHarness();
});
afterEach(async () => {
await harness.dispose();
});
it('fails validation when executed on Cars page instead of Track page', async () => {
await harness.navigateToFixtureStep(8);
await harness.adapter.getPage()?.waitForLoadState('domcontentloaded');
await expect(async () => {
await harness.executeStep(11, {});
}).rejects.toThrow(/Step 11 FAILED validation/i);
});
it('passes validation when on Track page', async () => {
await harness.navigateToFixtureStep(11);
await harness.adapter.getPage()?.waitForLoadState('domcontentloaded');
const result = await harness.executeStep(11, {});
expect(result.success).toBe(true);
});
});
});

View File

@@ -1,38 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
describe('Step 12 add track', () => {
let harness: StepHarness;
beforeEach(async () => {
harness = await createStepHarness();
});
afterEach(async () => {
await harness.dispose();
});
it('executes on Add Track modal from Track step', async () => {
await harness.navigateToFixtureStep(12);
const page = harness.adapter.getPage();
expect(page).not.toBeNull();
const sidebarTrack = await page!.textContent(
'#wizard-sidebar-link-set-track',
);
expect(sidebarTrack).toContain('Track');
const bodyText = await page!.textContent('body');
expect(bodyText).toMatch(/Add a Track/i);
const result = await harness.executeStep(12, {});
expect(result.success).toBe(true);
expect(result.error).toBeUndefined();
const footerText = await page!.textContent('.wizard-footer');
expect(footerText).toMatch(/Track Options/i);
});
});

View File

@@ -1,42 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
import { IRACING_SELECTORS } from 'core/automation/infrastructure//automation/dom/IRacingSelectors';
describe('Step 13 track options', () => {
let harness: StepHarness;
beforeEach(async () => {
harness = await createStepHarness();
});
afterEach(async () => {
await harness.dispose();
});
it('executes on Track Options page in mock wizard', async () => {
await harness.navigateToFixtureStep(13);
const page = harness.adapter.getPage();
expect(page).not.toBeNull();
const sidebarTrackOptions = await page!
.locator(IRACING_SELECTORS.wizard.sidebarLinks.trackOptions)
.first()
.innerText();
expect(sidebarTrackOptions).toMatch(/Track Options/i);
const trackOptionsContainer = page!
.locator(IRACING_SELECTORS.wizard.stepContainers.trackOptions)
.first();
expect(await trackOptionsContainer.count()).toBeGreaterThan(0);
const bodyText = await page!.textContent('body');
expect(bodyText).toContain('Create Starting Grid');
const result = await harness.executeStep(13, {});
expect(result.success).toBe(true);
expect(result.error).toBeUndefined();
});
});

View File

@@ -1,54 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
import { IRACING_SELECTORS } from 'core/automation/infrastructure//automation/dom/IRacingSelectors';
describe('Step 14 time of day', () => {
let harness: StepHarness;
beforeEach(async () => {
harness = await createStepHarness();
});
afterEach(async () => {
await harness.dispose();
});
it('executes on Time of Day page and applies time-of-day slider from config', async () => {
await harness.navigateToFixtureStep(14);
const page = harness.adapter.getPage();
expect(page).not.toBeNull();
const container = page!
.locator(IRACING_SELECTORS.wizard.stepContainers.timeOfDay)
.first();
expect(await container.count()).toBeGreaterThan(0);
const sidebarTimeOfDay = await page!.textContent(
'#wizard-sidebar-link-set-time-of-day',
);
expect(sidebarTimeOfDay).toContain('Time of Day');
const config = { timeOfDay: 800 };
const result = await harness.executeStep(14, config);
expect(result.success).toBe(true);
expect(result.error).toBeUndefined();
const timeSlider = page!
.locator(IRACING_SELECTORS.steps.timeOfDay)
.first();
const sliderExists = await timeSlider.count();
expect(sliderExists).toBeGreaterThan(0);
const valueAttr =
(await timeSlider.getAttribute('data-value')) ??
(await timeSlider.inputValue().catch(() => null));
expect(valueAttr).toBe(String(config.timeOfDay));
const footerText = await page!.textContent('.wizard-footer');
expect(footerText).toMatch(/Weather/i);
});
});

View File

@@ -1,71 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
import { IRACING_SELECTORS } from 'core/automation/infrastructure//automation/dom/IRacingSelectors';
describe('Step 15 weather', () => {
let harness: StepHarness;
beforeEach(async () => {
harness = await createStepHarness();
});
afterEach(async () => {
await harness.dispose();
});
it('executes on Weather page in mock wizard and applies weather config from JSON-backed controls', async () => {
await harness.navigateToFixtureStep(15);
const page = harness.adapter.getPage();
expect(page).not.toBeNull();
const sidebarWeather = await page!.textContent(
'#wizard-sidebar-link-set-weather',
);
expect(sidebarWeather).toContain('Weather');
const bodyText = await page!.textContent('body');
expect(bodyText).toMatch(/Weather Mode|Event weather/i);
const config = {
weatherType: '2',
temperature: 650,
};
const result = await harness.executeStep(15, config);
expect(result.success).toBe(true);
expect(result.error).toBeUndefined();
const weatherSelect = page!
.locator(IRACING_SELECTORS.steps.weatherType)
.first();
const weatherSelectCount = await weatherSelect.count();
if (weatherSelectCount > 0) {
const selectedWeatherValue =
(await weatherSelect.getAttribute('value')) ??
(await weatherSelect.textContent().catch(() => null));
expect(
(selectedWeatherValue ?? '').toLowerCase(),
).toMatch(/static|forecast|timeline|2/);
} else {
const radioGroup = page!.locator('[role="radiogroup"] input[type="radio"]').first();
const radioCount = await radioGroup.count();
expect(radioCount).toBeGreaterThan(0);
}
const tempSlider = page!
.locator(IRACING_SELECTORS.steps.temperature)
.first();
const tempExists = await tempSlider.count();
if (tempExists > 0) {
const tempValue =
(await tempSlider.getAttribute('data-value')) ??
(await tempSlider.inputValue().catch(() => null));
expect(tempValue).toBe(String(config.temperature));
}
});
});

View File

@@ -1,38 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
describe('Step 16 race options', () => {
let harness: StepHarness;
beforeEach(async () => {
harness = await createStepHarness();
});
afterEach(async () => {
await harness.dispose();
});
it('executes on Race Options page in mock wizard', async () => {
await harness.navigateToFixtureStep(16);
const page = harness.adapter.getPage();
expect(page).not.toBeNull();
const sidebarRaceOptions = await page!.textContent(
'#wizard-sidebar-link-set-race-options',
);
expect(sidebarRaceOptions).toContain('Race Options');
const bodyText = await page!.textContent('body');
expect(bodyText).toMatch(/No Incident Penalty|Select Discipline/i);
const result = await harness.executeStep(16, {});
expect(result.success).toBe(true);
expect(result.error).toBeUndefined();
const footerText = await page!.textContent('.wizard-footer');
expect(footerText).toMatch(/Track Conditions/i);
});
});

View File

@@ -1,54 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
import { CheckoutConfirmation } from 'apps/companion/main/automation/domain/value-objects/CheckoutConfirmation';
describe('Step 17 team driving', () => {
let harness: StepHarness;
beforeEach(async () => {
harness = await createStepHarness();
});
afterEach(async () => {
await harness.dispose();
});
it('executes on Team Driving page and completes without checkout', async () => {
await harness.navigateToFixtureStep(17);
const page = harness.adapter.getPage();
expect(page).not.toBeNull();
const bodyText = await page!.textContent('body');
expect(bodyText).toMatch(/Team Driving|Track Conditions/i);
const result = await harness.executeStep(17, {
trackState: 'medium',
});
expect(result.success).toBe(true);
expect(result.error).toBeUndefined();
});
it('requests checkout confirmation and uses the user decision', async () => {
await harness.navigateToFixtureStep(17);
let called = false;
harness.adapter.setCheckoutConfirmationCallback(async (price, state) => {
called = true;
expect(price).toBeDefined();
expect(state).toBeDefined();
return CheckoutConfirmation.create('confirmed');
});
const result = await harness.executeStep(17, {
trackState: 'medium',
});
expect(result.success).toBe(true);
expect(result.error).toBeUndefined();
expect(called).toBe(true);
});
});

View File

@@ -1,33 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
describe('Step 18 track conditions (manual stop)', () => {
let harness: StepHarness;
beforeEach(async () => {
harness = await createStepHarness();
});
afterEach(async () => {
await harness.dispose();
});
it('treats Track Conditions as manual stop without invoking automation step 18', async () => {
await harness.navigateToFixtureStep(18);
const page = harness.adapter.getPage();
expect(page).not.toBeNull();
const sidebarTrackConditions = await page!.textContent(
'#wizard-sidebar-link-set-track-conditions',
);
expect(sidebarTrackConditions).toContain('Track Conditions');
const trackConditionsContainer = page!.locator('#set-track-conditions').first();
expect(await trackConditionsContainer.count()).toBeGreaterThan(0);
const bodyText = await page!.textContent('body');
expect(bodyText).toMatch(/Track Conditions|Starting Track State/i);
});
});

View File

@@ -1,20 +0,0 @@
import { StepId } from 'apps/companion/main/automation/domain/value-objects/StepId';
import type { PlaywrightAutomationAdapter } from 'core/automation/infrastructure//automation';
import type { AutomationResult } from 'apps/companion/main/automation/application/ports/AutomationResults';
export function assertAutoNavigationConfig(config: Record<string, unknown>): void {
const skipFixtureNavigationFlag =
(config as { __skipFixtureNavigation?: unknown }).__skipFixtureNavigation;
if (skipFixtureNavigationFlag === true) {
throw new Error('__skipFixtureNavigation is forbidden in auto-navigation suites');
}
}
export async function executeStepWithAutoNavigationGuard(
adapter: PlaywrightAutomationAdapter,
step: number,
config: Record<string, unknown>,
): Promise<AutomationResult> {
assertAutoNavigationConfig(config);
return adapter.executeStep(StepId.create(step), config);
}

View File

@@ -1,284 +0,0 @@
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
/**
* Permission status for E2E tests requiring real automation.
*/
export interface E2EPermissionStatus {
accessibility: boolean;
screenRecording: boolean;
platform: NodeJS.Platform;
isCI: boolean;
isHeadless: boolean;
canRunRealAutomation: boolean;
}
/**
* Result of permission check with actionable information.
*/
export interface PermissionCheckResult {
canProceed: boolean;
shouldSkip: boolean;
skipReason?: string;
status: E2EPermissionStatus;
warnings: string[];
}
/**
* PermissionGuard for E2E tests.
*
* Checks macOS Accessibility and Screen Recording permissions
* required for real native automation. Provides graceful skip
* logic for CI environments or when permissions are unavailable.
*/
export class PermissionGuard {
private cachedStatus: E2EPermissionStatus | null = null;
/**
* Check all permissions and determine if real automation tests can run.
*
* @returns PermissionCheckResult with status and skip reason if applicable
*/
async checkPermissions(): Promise<PermissionCheckResult> {
const status = await this.getPermissionStatus();
const warnings: string[] = [];
// CI environments should always skip real automation tests
if (status.isCI) {
return {
canProceed: false,
shouldSkip: true,
skipReason: 'Running in CI environment - real automation tests require a display',
status,
warnings: ['CI environment detected, skipping real automation tests'],
};
}
// Headless environments cannot run real automation
if (status.isHeadless) {
return {
canProceed: false,
shouldSkip: true,
skipReason: 'Running in headless environment - real automation tests require a display',
status,
warnings: ['Headless environment detected, skipping real automation tests'],
};
}
// macOS-specific permission checks
if (status.platform === 'darwin') {
if (!status.accessibility) {
warnings.push('macOS Accessibility permission not granted');
warnings.push('To grant: System Preferences > Security & Privacy > Privacy > Accessibility');
}
if (!status.screenRecording) {
warnings.push('macOS Screen Recording permission not granted');
warnings.push('To grant: System Preferences > Security & Privacy > Privacy > Screen Recording');
}
if (!status.accessibility || !status.screenRecording) {
return {
canProceed: false,
shouldSkip: true,
skipReason: `Missing macOS permissions: ${[
!status.accessibility && 'Accessibility',
!status.screenRecording && 'Screen Recording',
].filter(Boolean).join(', ')}`,
status,
warnings,
};
}
}
// All checks passed
return {
canProceed: true,
shouldSkip: false,
status,
warnings,
};
}
/**
* Get the current permission status.
*/
async getPermissionStatus(): Promise<E2EPermissionStatus> {
if (this.cachedStatus) {
return this.cachedStatus;
}
const platform = process.platform;
const isCI = this.detectCI();
const isHeadless = await this.detectHeadless();
let accessibility = true;
let screenRecording = true;
if (platform === 'darwin') {
accessibility = await this.checkMacOSAccessibility();
screenRecording = await this.checkMacOSScreenRecording();
}
const canRunRealAutomation =
!isCI &&
!isHeadless &&
accessibility &&
screenRecording;
this.cachedStatus = {
accessibility,
screenRecording,
platform,
isCI,
isHeadless,
canRunRealAutomation,
};
return this.cachedStatus;
}
/**
* Detect if running in a CI environment.
*/
private detectCI(): boolean {
return !!(
process.env.CI ||
process.env.CONTINUOUS_INTEGRATION ||
process.env.GITHUB_ACTIONS ||
process.env.GITLAB_CI ||
process.env.CIRCLECI ||
process.env.TRAVIS ||
process.env.JENKINS_URL ||
process.env.BUILDKITE ||
process.env.TF_BUILD // Azure DevOps
);
}
/**
* Detect if running in a headless environment (no display).
*/
private async detectHeadless(): Promise<boolean> {
// Check for explicit headless environment variable
if (process.env.HEADLESS === 'true' || process.env.DISPLAY === '') {
return true;
}
// On Linux, check if DISPLAY is set
if (process.platform === 'linux' && !process.env.DISPLAY) {
return true;
}
// On macOS, check if we're in a non-GUI session
if (process.platform === 'darwin') {
try {
// Check if we can access the WindowServer
const { stdout } = await execAsync('pgrep -x WindowServer');
return !stdout.trim();
} catch {
// pgrep returns non-zero if no process found
return true;
}
}
return false;
}
/**
* Check macOS Accessibility permission without Electron.
* Uses AppleScript to test if we can control system events.
*/
private async checkMacOSAccessibility(): Promise<boolean> {
try {
// Try to use AppleScript to check accessibility
// This will fail if accessibility permission is not granted
await execAsync(`osascript -e 'tell application "System Events" to return name of first process'`);
return true;
} catch {
// Permission denied or System Events not accessible
return false;
}
}
/**
* Check macOS Screen Recording permission without Electron.
* Uses `screencapture` heuristics to detect denial.
*/
private async checkMacOSScreenRecording(): Promise<boolean> {
try {
const { stderr } = await execAsync('screencapture -x -c 2>&1 || true');
if (stderr.includes('permission') || stderr.includes('denied')) {
return false;
}
return true;
} catch {
return false;
}
}
/**
* Clear cached permission status.
*/
clearCache(): void {
this.cachedStatus = null;
}
/**
* Format permission status for logging.
*/
formatStatus(status: E2EPermissionStatus): string {
const lines = [
`Platform: ${status.platform}`,
`CI Environment: ${status.isCI ? 'Yes' : 'No'}`,
`Headless: ${status.isHeadless ? 'Yes' : 'No'}`,
`Accessibility Permission: ${status.accessibility ? '✓' : '✗'}`,
`Screen Recording Permission: ${status.screenRecording ? '✓' : '✗'}`,
`Can Run Real Automation: ${status.canRunRealAutomation ? '✓' : '✗'}`,
];
return lines.join('\n');
}
}
/**
* Singleton instance for use in tests.
*/
export const permissionGuard = new PermissionGuard();
/**
* Skip helper for Cucumber tests.
* Call in Before hook to skip tests if permissions are unavailable.
*
* @returns Skip reason if tests should be skipped, undefined otherwise
*/
export async function shouldSkipRealAutomationTests(): Promise<string | undefined> {
const result = await permissionGuard.checkPermissions();
if (result.shouldSkip) {
console.warn('\n⚠ Skipping real automation tests:');
console.warn(` ${result.skipReason}`);
if (result.warnings.length > 0) {
result.warnings.forEach(w => console.warn(` - ${w}`));
}
return result.skipReason;
}
return undefined;
}
/**
* Assert that real automation can proceed.
* Throws an error with detailed information if not.
*/
export async function assertCanRunRealAutomation(): Promise<void> {
const result = await permissionGuard.checkPermissions();
if (!result.canProceed) {
const status = permissionGuard.formatStatus(result.status);
throw new Error(
`Cannot run real automation tests:\n${result.skipReason}\n\nPermission Status:\n${status}`
);
}
}

View File

@@ -1,127 +0,0 @@
import type { AutomationResult } from 'apps/companion/main/automation/application/ports/AutomationResults';
import { StepId } from 'apps/companion/main/automation/domain/value-objects/StepId';
import {
PlaywrightAutomationAdapter,
FixtureServer,
} from 'core/automation/infrastructure//automation';
import { PinoLogAdapter } from 'core/automation/infrastructure//logging/PinoLogAdapter';
export interface StepHarness {
server: FixtureServer;
adapter: PlaywrightAutomationAdapter;
baseUrl: string;
getFixtureUrl(step: number): string;
navigateToFixtureStep(step: number): Promise<void>;
executeStep(step: number, config: Record<string, unknown>): Promise<AutomationResult>;
executeStepWithAutoNavigation(
step: number,
config: Record<string, unknown>,
): Promise<AutomationResult>;
executeStepWithFixtureMismatch(
step: number,
config: Record<string, unknown>,
): Promise<AutomationResult>;
dispose(): Promise<void>;
}
async function createRealAdapter(baseUrl: string): Promise<PlaywrightAutomationAdapter> {
const logger = new PinoLogAdapter();
const adapter = new PlaywrightAutomationAdapter(
{
headless: true,
timeout: 8000,
mode: 'real',
baseUrl,
userDataDir: '',
},
logger,
);
const result = await adapter.connect(false);
if (!result.success) {
throw new Error(result.error || 'Failed to connect Playwright adapter');
}
return adapter;
}
async function createMockAdapter(): Promise<PlaywrightAutomationAdapter> {
const logger = new PinoLogAdapter();
const adapter = new PlaywrightAutomationAdapter(
{
headless: true,
timeout: 5000,
mode: 'mock',
},
logger,
);
const result = await adapter.connect();
if (!result.success) {
throw new Error(result.error || 'Failed to connect mock Playwright adapter');
}
return adapter;
}
export async function createStepHarness(useMock: boolean = false): Promise<StepHarness> {
const server = new FixtureServer();
const { url } = await server.start();
const adapter = useMock ? await createMockAdapter() : await createRealAdapter(url);
async function navigateToFixtureStep(step: number): Promise<void> {
await adapter.navigateToPage(server.getFixtureUrl(step));
await adapter.getPage()?.waitForLoadState('domcontentloaded');
}
async function executeStepWithAutoNavigation(
step: number,
config: Record<string, unknown>,
): Promise<AutomationResult> {
const skipFixtureNavigationFlag =
(config as { __skipFixtureNavigation?: unknown }).__skipFixtureNavigation;
if (skipFixtureNavigationFlag === true) {
throw new Error(
'__skipFixtureNavigation is not allowed in auto-navigation path',
);
}
return adapter.executeStep(StepId.create(step), config);
}
async function executeStepWithFixtureMismatch(
step: number,
config: Record<string, unknown>,
): Promise<AutomationResult> {
return adapter.executeStep(StepId.create(step), {
...config,
__skipFixtureNavigation: true,
});
}
async function executeStep(
step: number,
config: Record<string, unknown>,
): Promise<AutomationResult> {
return executeStepWithFixtureMismatch(step, config);
}
async function dispose(): Promise<void> {
await adapter.disconnect();
await server.stop();
}
return {
server,
adapter,
baseUrl: url,
getFixtureUrl: (step) => server.getFixtureUrl(step),
navigateToFixtureStep,
executeStep,
executeStepWithAutoNavigation,
executeStepWithFixtureMismatch,
dispose,
};
}

View File

@@ -1,87 +0,0 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import {
PlaywrightAutomationAdapter,
FixtureServer,
} from 'core/automation/infrastructure//automation';
import { StepId } from 'apps/companion/main/automation/domain/value-objects/StepId';
import { PinoLogAdapter } from 'core/automation/infrastructure//logging/PinoLogAdapter';
import { executeStepWithAutoNavigationGuard } from '../support/AutoNavGuard';
describe('Hosted validator guards (fixture-backed, real stack)', () => {
let server: FixtureServer;
let adapter: PlaywrightAutomationAdapter;
let baseUrl: string;
beforeAll(async () => {
server = new FixtureServer();
const info = await server.start();
baseUrl = info.url;
const logger = new PinoLogAdapter();
adapter = new PlaywrightAutomationAdapter(
{
headless: true,
timeout: 15_000,
baseUrl,
mode: 'real',
userDataDir: '',
},
logger,
);
const result = await adapter.connect(false);
expect(result.success).toBe(true);
expect(adapter.isConnected()).toBe(true);
}, 120_000);
afterAll(async () => {
if (adapter) {
await adapter.disconnect();
}
if (server) {
await server.stop();
}
});
it(
'runs a short hosted sequence (3 → 4 → 5) with autonav and no validator failures',
async () => {
await adapter.navigateToPage(server.getFixtureUrl(3));
const step3Result = await executeStepWithAutoNavigationGuard(adapter, 3, {
sessionName: 'Validator happy-path session',
password: 'validator',
description: 'Validator autonav slice',
});
expect(step3Result.success).toBe(true);
await adapter.navigateToPage(server.getFixtureUrl(4));
const step4Result = await executeStepWithAutoNavigationGuard(adapter, 4, {
region: 'US',
startNow: true,
});
expect(step4Result.success).toBe(true);
await adapter.navigateToPage(server.getFixtureUrl(5));
const step5Result = await executeStepWithAutoNavigationGuard(adapter, 5, {});
expect(step5Result.success).toBe(true);
},
120_000,
);
it(
'fails clearly when executing a mismatched step on the wrong page (validator wiring)',
async () => {
await adapter.navigateToPage(server.getFixtureUrl(8));
const stepId = StepId.create(11);
await expect(
adapter.executeStep(stepId, {
trackSearch: 'Spa',
__skipFixtureNavigation: true,
}),
).rejects.toThrow(/Step 11 FAILED validation|validation error/i);
},
120_000,
);
});

View File

@@ -1,102 +0,0 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import {
PlaywrightAutomationAdapter,
FixtureServer,
} from 'core/automation/infrastructure//automation';
import { StepId } from 'apps/companion/main/automation/domain/value-objects/StepId';
import { PinoLogAdapter } from 'core/automation/infrastructure//logging/PinoLogAdapter';
import { IRACING_SELECTORS } from 'core/automation/infrastructure//automation/dom/IRacingSelectors';
import { executeStepWithAutoNavigationGuard } from '../support/AutoNavGuard';
describe('Workflow hosted session autonav slice (fixture-backed, real stack)', () => {
let server: FixtureServer;
let adapter: PlaywrightAutomationAdapter;
let baseUrl: string;
beforeAll(async () => {
server = new FixtureServer();
const info = await server.start();
baseUrl = info.url;
const logger = new PinoLogAdapter();
adapter = new PlaywrightAutomationAdapter(
{
headless: true,
timeout: 15_000,
baseUrl,
mode: 'real',
userDataDir: '',
},
logger,
);
const result = await adapter.connect(false);
expect(result.success).toBe(true);
expect(adapter.isConnected()).toBe(true);
});
afterAll(async () => {
await adapter.disconnect();
await server.stop();
});
async function expectStepOnContainer(
expectedContainer: keyof typeof IRACING_SELECTORS.wizard.stepContainers,
) {
const page = adapter.getPage();
expect(page).not.toBeNull();
const selector = IRACING_SELECTORS.wizard.stepContainers[expectedContainer];
const container = page!.locator(selector).first();
await container.waitFor({ state: 'attached', timeout: 10_000 });
expect(await container.count()).toBeGreaterThan(0);
}
it(
'navigates via autonav across representative steps (1 → 3 → 7 → 9 → 13 → 17)',
async () => {
await adapter.navigateToPage(server.getFixtureUrl(1));
const step1Result = await executeStepWithAutoNavigationGuard(adapter, 1, {});
expect(step1Result.success).toBe(true);
await adapter.navigateToPage(server.getFixtureUrl(3));
const step3Result = await executeStepWithAutoNavigationGuard(adapter, 3, {
sessionName: 'Autonav workflow session',
password: 'autonav',
description: 'Fixture-backed autonav slice',
});
expect(step3Result.success).toBe(true);
await expectStepOnContainer('raceInformation');
await adapter.navigateToPage(server.getFixtureUrl(7));
const step7Result = await executeStepWithAutoNavigationGuard(adapter, 7, {
practice: 10,
qualify: 10,
race: 20,
});
expect(step7Result.success).toBe(true);
await expectStepOnContainer('timeLimit');
await adapter.navigateToPage(server.getFixtureUrl(9));
const step9Result = await executeStepWithAutoNavigationGuard(adapter, 9, {
carSearch: 'Acura ARX-06',
});
expect(step9Result.success).toBe(true);
await expectStepOnContainer('cars');
await adapter.navigateToPage(server.getFixtureUrl(13));
const step13Result = await executeStepWithAutoNavigationGuard(adapter, 13, {
trackSearch: 'Spa',
});
expect(step13Result.success).toBe(true);
await expectStepOnContainer('trackOptions');
await adapter.navigateToPage(server.getFixtureUrl(17));
const step17Result = await executeStepWithAutoNavigationGuard(adapter, 17, {
trackState: 'medium',
});
expect(step17Result.success).toBe(true);
await expectStepOnContainer('raceOptions');
},
120_000,
);
});

View File

@@ -1,98 +0,0 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import {
PlaywrightAutomationAdapter,
FixtureServer,
} from 'core/automation/infrastructure//automation';
import { InMemorySessionRepository } from 'apps/companion/main/automation/infrastructure/repositories/InMemorySessionRepository';
import { AutomationEngineAdapter } from 'core/automation/infrastructure//automation/engine/AutomationEngineAdapter';
import { StartAutomationSessionUseCase } from 'apps/companion/main/automation/application/use-cases/StartAutomationSessionUseCase';
import { StepId } from 'apps/companion/main/automation/domain/value-objects/StepId';
import { PinoLogAdapter } from 'core/automation/infrastructure//logging/PinoLogAdapter';
describe('Workflow hosted session end-to-end (fixture-backed, real stack)', () => {
let server: FixtureServer;
let adapter: PlaywrightAutomationAdapter;
let baseUrl: string;
beforeAll(async () => {
server = new FixtureServer();
const info = await server.start();
baseUrl = info.url;
const logger = new PinoLogAdapter();
adapter = new PlaywrightAutomationAdapter(
{
headless: true,
timeout: 10_000,
baseUrl,
mode: 'real',
userDataDir: '',
},
logger,
);
const connectResult = await adapter.connect(false);
expect(connectResult.success).toBe(true);
expect(adapter.isConnected()).toBe(true);
});
afterAll(async () => {
await adapter.disconnect();
await server.stop();
});
function createRealEngine() {
const repository = new InMemorySessionRepository();
const engine = new AutomationEngineAdapter(adapter, repository);
const useCase = new StartAutomationSessionUseCase(engine, adapter, repository);
return { repository, engine, useCase };
}
it(
'runs 117 from use case and stops automation at manual Track Conditions (STOPPED_AT_STEP_18)',
async () => {
const { repository, engine, useCase } = createRealEngine();
const config: any = {
sessionName: 'Fixture E2E full workflow (real stack)',
trackId: 'spa',
carIds: ['dallara-f3'],
};
const dto = await useCase.execute(config);
expect(dto.state).toBe('PENDING');
expect(dto.currentStep).toBe(1);
await adapter.navigateToPage(server.getFixtureUrl(1));
await engine.executeStep(StepId.create(1), config);
const deadline = Date.now() + 60_000;
let finalSession = null;
// eslint-disable-next-line no-constant-condition
while (true) {
const sessions = await repository.findAll();
finalSession = sessions[0] ?? null;
if (finalSession && finalSession.state.isStoppedAtStep18()) {
break;
}
if (Date.now() > deadline) {
throw new Error('Timed out waiting for automation workflow to complete');
}
await new Promise((resolve) => setTimeout(resolve, 250));
}
expect(finalSession).not.toBeNull();
expect(finalSession!.state.isStoppedAtStep18()).toBe(true);
expect(finalSession!.currentStep.value).toBe(17);
expect(finalSession!.startedAt).toBeInstanceOf(Date);
expect(finalSession!.completedAt).toBeInstanceOf(Date);
expect(finalSession!.errorMessage).toBeUndefined();
},
);
});

View File

@@ -1,93 +0,0 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import {
PlaywrightAutomationAdapter,
FixtureServer,
} from 'core/automation/infrastructure//automation';
import { StepId } from 'apps/companion/main/automation/domain/value-objects/StepId';
import { IRACING_SELECTORS } from 'core/automation/infrastructure//automation/dom/IRacingSelectors';
import { PinoLogAdapter } from 'core/automation/infrastructure//logging/PinoLogAdapter';
describe('Workflow steps 79 cars flow (fixture-backed, real stack)', () => {
let adapter: PlaywrightAutomationAdapter;
let server: FixtureServer;
let baseUrl: string;
beforeAll(async () => {
server = new FixtureServer();
const info = await server.start();
baseUrl = info.url;
const logger = new PinoLogAdapter();
adapter = new PlaywrightAutomationAdapter(
{
headless: true,
timeout: 8000,
baseUrl,
mode: 'real',
userDataDir: '',
},
logger,
);
const result = await adapter.connect(false);
expect(result.success).toBe(true);
expect(adapter.isConnected()).toBe(true);
});
afterAll(async () => {
await adapter.disconnect();
await server.stop();
});
it(
'executes time limits, cars, and add car in sequence using fixtures and leaves DOM-backed state',
async () => {
await adapter.navigateToPage(server.getFixtureUrl(7));
const step7Result = await adapter.executeStep(StepId.create(7), {
practice: 10,
qualify: 10,
race: 20,
});
expect(step7Result.success).toBe(true);
const page = adapter.getPage();
expect(page).not.toBeNull();
const raceSlider = page!
.locator(IRACING_SELECTORS.steps.race)
.first();
const raceSliderValue =
(await raceSlider.getAttribute('data-value')) ??
(await raceSlider.inputValue().catch(() => null));
expect(raceSliderValue).toBe('20');
await adapter.navigateToPage(server.getFixtureUrl(8));
const step8Result = await adapter.executeStep(StepId.create(8), {});
expect(step8Result.success).toBe(true);
const carsContainer = page!
.locator(IRACING_SELECTORS.wizard.stepContainers.cars)
.first();
expect(await carsContainer.count()).toBeGreaterThan(0);
const addCarButton = page!
.locator(IRACING_SELECTORS.steps.addCarButton)
.first();
expect(await addCarButton.count()).toBeGreaterThan(0);
await adapter.navigateToPage(server.getFixtureUrl(9));
const step9Result = await adapter.executeStep(StepId.create(9), {
carSearch: 'Acura ARX-06',
});
expect(step9Result.success).toBe(true);
const carsTable = page!
.locator('#select-car-set-cars table.table.table-striped')
.first();
expect(await carsTable.count()).toBeGreaterThan(0);
const acuraCell = carsTable.locator('tbody tr td >> text=Acura ARX-06 GTP');
expect(await acuraCell.count()).toBeGreaterThan(0);
},
);
});