remove companion tests
This commit is contained in:
@@ -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 2–7)', () => {
|
||||
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,
|
||||
);
|
||||
});
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
@@ -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
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
@@ -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 {};
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
@@ -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 1–17 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();
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -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 7–9 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);
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user