wip
This commit is contained in:
@@ -1243,4 +1243,46 @@ GridPilot's testing strategy ensures:
|
|||||||
- **Browser automation works correctly** (Docker E2E tests with real fixtures)
|
- **Browser automation works correctly** (Docker E2E tests with real fixtures)
|
||||||
- **OS-level automation works correctly** (Native macOS E2E tests with display access)
|
- **OS-level automation works correctly** (Native macOS E2E tests with display access)
|
||||||
|
|
||||||
By following BDD principles and maintaining clear test organization, the team can confidently evolve GridPilot while preserving correctness and stability.
|
### Hosted Session Automation Test Pyramid
|
||||||
|
|
||||||
|
For the iRacing hosted-session automation, confidence is provided by these concrete suites:
|
||||||
|
|
||||||
|
- **Domain/Application unit tests**
|
||||||
|
- Entities and value objects such as [`AutomationSession`](packages/domain/entities/AutomationSession.ts:1), [`SessionState`](packages/domain/value-objects/SessionState.ts:1), [`CheckoutState`](packages/domain/value-objects/CheckoutState.ts:1), [`CheckoutConfirmation`](packages/domain/value-objects/CheckoutConfirmation.ts:1), and [`RaceCreationResult`](packages/domain/value-objects/RaceCreationResult.ts:1).
|
||||||
|
- Use cases such as [`StartAutomationSessionUseCase`](packages/application/use-cases/StartAutomationSessionUseCase.ts:1), [`VerifyAuthenticatedPageUseCase`](packages/application/use-cases/VerifyAuthenticatedPageUseCase.ts:1), [`CompleteRaceCreationUseCase`](packages/application/use-cases/CompleteRaceCreationUseCase.ts:1), and [`ConfirmCheckoutUseCase`](packages/application/use-cases/ConfirmCheckoutUseCase.ts:1).
|
||||||
|
|
||||||
|
- **Infrastructure / automation integration tests**
|
||||||
|
- Adapter wiring and fixture serving via [`FixtureServer.integration.test.ts`](tests/integration/infrastructure/FixtureServer.integration.test.ts:1).
|
||||||
|
- Playwright lifecycle and overlay wiring such as [`BrowserModeIntegration.test.ts`](tests/integration/infrastructure/BrowserModeIntegration.test.ts:1) and automation-flow tests like [`CarsFlow.integration.test.ts`](tests/integration/infrastructure/automation/CarsFlow.integration.test.ts:1).
|
||||||
|
- UI/overlay integration via [`renderer-overlay.integration.test.ts`](tests/integration/interface/renderer/renderer-overlay.integration.test.ts:1).
|
||||||
|
|
||||||
|
- **Fixture-based step E2E tests (per-step behavior)**
|
||||||
|
- One test per wizard step under [`tests/e2e/steps`](tests/e2e/steps:1), all using the shared [`StepHarness`](tests/e2e/support/StepHarness.ts:1) and [`FixtureServer`](packages/infrastructure/adapters/automation/engine/FixtureServer.ts:1).
|
||||||
|
- These validate DOM-level selectors / flows for each step (1–18) against fixture-backed HTML, and are considered canonical for step behavior.
|
||||||
|
|
||||||
|
- **Fixture-based workflow E2E tests (button → auth entry → automation → confirmation boundary)**
|
||||||
|
- Workflow-focused tests under [`tests/e2e/workflows`](tests/e2e/workflows:1) that drive the `PlaywrightAutomationAdapter` + `WizardStepOrchestrator` across multiple steps using the fixture server.
|
||||||
|
- Example: [`steps-07-09-cars-flow.e2e.test.ts`](tests/e2e/workflows/steps-07-09-cars-flow.e2e.test.ts:1) exercises cross-step cars flow, while [`full-hosted-session.workflow.e2e.test.ts`](tests/e2e/workflows/full-hosted-session.workflow.e2e.test.ts:1) runs a full 1–18 workflow via [`MockAutomationEngineAdapter`](packages/infrastructure/adapters/automation/engine/MockAutomationEngineAdapter.ts:1) and [`StartAutomationSessionUseCase`](packages/application/use-cases/StartAutomationSessionUseCase.ts:1), asserting final `SessionState` and step position.
|
||||||
|
- Additional workflow scenarios cover mid-flow failure using [`MockBrowserAutomationAdapter`](packages/infrastructure/adapters/automation/engine/MockBrowserAutomationAdapter.ts:1), ensuring failure states and diagnostics are surfaced without emitting false confirmations.
|
||||||
|
|
||||||
|
- **Opt-in real-world automation smoke tests (true iRacing / NutJs)**
|
||||||
|
- The legacy real iRacing automation suite [`automation.e2e.test.ts`](tests/e2e/automation.e2e.test.ts:1) is treated as a smoke-only, opt-in layer.
|
||||||
|
- It is gated by `RUN_REAL_AUTOMATION_SMOKE=1` and should not run in normal CI; it exists to validate that NutJs / template-based automation still matches the real UI for a small number of manual smoke runs on a prepared macOS environment.
|
||||||
|
|
||||||
|
#### Confidence expectations
|
||||||
|
|
||||||
|
- For **normal changes** to hosted-session automation (selectors, step logic, overlay behavior, authentication, or confirmation flows), the following suites must pass to claim "high confidence":
|
||||||
|
- All relevant **unit tests** in `tests/unit` that touch the changed domain/use-case code.
|
||||||
|
- All relevant **integration tests** in `tests/integration` for the affected adapters.
|
||||||
|
- All **step E2E tests** under [`tests/e2e/steps`](tests/e2e/steps:1).
|
||||||
|
- All **workflow E2E tests** under [`tests/e2e/workflows`](tests/e2e/workflows:1).
|
||||||
|
|
||||||
|
- The **real-world smoke suite** in [`tests/e2e/automation.e2e.test.ts`](tests/e2e/automation.e2e.test.ts:1) is an additional, manual confidence layer and should be run only on configured machines when validating large changes to NutJs automation, template packs, or iRacing UI assumptions.
|
||||||
|
|
||||||
|
- When adding new behavior:
|
||||||
|
- Prefer **unit tests** for domain/application changes.
|
||||||
|
- Add or extend **integration tests** when introducing new adapters or external integration.
|
||||||
|
- Add **step E2E tests** when changing DOM/step behavior for a specific wizard step.
|
||||||
|
- Add or extend **workflow E2E tests** when behavior spans multiple steps, touches authentication/session lifecycle, or affects confirmation/checkout behavior end-to-end.
|
||||||
|
|
||||||
|
By following BDD principles and maintaining clear test organization, the team can confidently evolve GridPilot while preserving correctness and stability, with a dedicated, layered confidence story for hosted-session automation.
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Browser, Page, BrowserContext } from 'playwright';
|
import type { Browser, Page, BrowserContext } from 'playwright';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { extractDom, ExportedElement } from '../../../../scripts/dom-export/domExtractor';
|
||||||
import { StepId } from '../../../../domain/value-objects/StepId';
|
import { StepId } from '../../../../domain/value-objects/StepId';
|
||||||
import { AuthenticationState } from '../../../../domain/value-objects/AuthenticationState';
|
import { AuthenticationState } from '../../../../domain/value-objects/AuthenticationState';
|
||||||
import { BrowserAuthenticationState } from '../../../../domain/value-objects/BrowserAuthenticationState';
|
import { BrowserAuthenticationState } from '../../../../domain/value-objects/BrowserAuthenticationState';
|
||||||
@@ -22,6 +23,7 @@ import { Result } from '../../../../shared/result/Result';
|
|||||||
import { IRACING_SELECTORS, IRACING_URLS, IRACING_TIMEOUTS, ALL_BLOCKED_SELECTORS, BLOCKED_KEYWORDS } from '../dom/IRacingSelectors';
|
import { IRACING_SELECTORS, IRACING_URLS, IRACING_TIMEOUTS, ALL_BLOCKED_SELECTORS, BLOCKED_KEYWORDS } from '../dom/IRacingSelectors';
|
||||||
import { SessionCookieStore } from '../auth/SessionCookieStore';
|
import { SessionCookieStore } from '../auth/SessionCookieStore';
|
||||||
import { PlaywrightBrowserSession } from './PlaywrightBrowserSession';
|
import { PlaywrightBrowserSession } from './PlaywrightBrowserSession';
|
||||||
|
import { getFixtureForStep } from '../engine/FixtureServer';
|
||||||
import { BrowserModeConfigLoader, BrowserMode } from '../../../config/BrowserModeConfig';
|
import { BrowserModeConfigLoader, BrowserMode } from '../../../config/BrowserModeConfig';
|
||||||
import { getAutomationMode } from '../../../config/AutomationConfig';
|
import { getAutomationMode } from '../../../config/AutomationConfig';
|
||||||
import { PageStateValidator, PageStateValidation, PageStateValidationResult } from '../../../../domain/services/PageStateValidator';
|
import { PageStateValidator, PageStateValidation, PageStateValidationResult } from '../../../../domain/services/PageStateValidator';
|
||||||
@@ -645,6 +647,21 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.syncSessionStateFromBrowser();
|
this.syncSessionStateFromBrowser();
|
||||||
|
|
||||||
|
if (this.config.mode === 'mock' && this.config.baseUrl) {
|
||||||
|
try {
|
||||||
|
const fixture = getFixtureForStep(1);
|
||||||
|
if (fixture) {
|
||||||
|
const base = this.config.baseUrl.replace(/\/$/, '');
|
||||||
|
await this.navigator.navigateToPage(`${base}/${fixture}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.log('debug', 'Initial fixture navigation failed (mock mode)', {
|
||||||
|
error: String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -871,13 +888,13 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
|||||||
*
|
*
|
||||||
* Error dumps are always kept and not subject to cleanup.
|
* Error dumps are always kept and not subject to cleanup.
|
||||||
*/
|
*/
|
||||||
private async saveDebugInfo(stepName: string, error: Error): Promise<{ screenshotPath?: string; htmlPath?: string }> {
|
private async saveDebugInfo(stepName: string, error: Error): Promise<{ screenshotPath?: string; htmlPath?: string; domPath?: string }> {
|
||||||
if (!this.page) return {};
|
if (!this.page) return {};
|
||||||
|
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
const baseName = `debug-error-${stepName}-${timestamp}`;
|
const baseName = `debug-error-${stepName}-${timestamp}`;
|
||||||
const debugDir = path.join(process.cwd(), 'debug-screenshots');
|
const debugDir = path.join(process.cwd(), 'debug-screenshots');
|
||||||
const result: { screenshotPath?: string; htmlPath?: string } = {};
|
const result: { screenshotPath?: string; htmlPath?: string; domPath?: string } = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.promises.mkdir(debugDir, { recursive: true });
|
await fs.promises.mkdir(debugDir, { recursive: true });
|
||||||
@@ -913,6 +930,22 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
|||||||
await fs.promises.writeFile(htmlPath, html);
|
await fs.promises.writeFile(htmlPath, html);
|
||||||
result.htmlPath = htmlPath;
|
result.htmlPath = htmlPath;
|
||||||
this.log('error', `Error debug HTML saved: ${htmlPath}`, { path: htmlPath });
|
this.log('error', `Error debug HTML saved: ${htmlPath}`, { path: htmlPath });
|
||||||
|
|
||||||
|
// Save structural DOM export alongside HTML
|
||||||
|
try {
|
||||||
|
await this.page.evaluate(() => {
|
||||||
|
(window as any).__name = (window as any).__name || ((fn: any) => fn);
|
||||||
|
});
|
||||||
|
const items = (await this.page.evaluate(
|
||||||
|
extractDom as () => ExportedElement[]
|
||||||
|
)) as unknown as ExportedElement[];
|
||||||
|
const domPath = path.join(debugDir, `${baseName}.dom.json`);
|
||||||
|
await fs.promises.writeFile(domPath, JSON.stringify(items, null, 2), 'utf8');
|
||||||
|
result.domPath = domPath;
|
||||||
|
this.log('error', `Error debug DOM saved: ${domPath}`, { path: domPath });
|
||||||
|
} catch (domErr) {
|
||||||
|
this.log('warn', 'Failed to save error debug DOM', { error: String(domErr) });
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.log('warn', 'Failed to save error debug info', { error: String(e) });
|
this.log('warn', 'Failed to save error debug info', { error: String(e) });
|
||||||
}
|
}
|
||||||
@@ -927,13 +960,13 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
|||||||
* Files are named with "before-step-N" prefix and old snapshots are cleaned up
|
* Files are named with "before-step-N" prefix and old snapshots are cleaned up
|
||||||
* to avoid disk bloat (keeps only last MAX_BEFORE_SNAPSHOTS).
|
* to avoid disk bloat (keeps only last MAX_BEFORE_SNAPSHOTS).
|
||||||
*/
|
*/
|
||||||
private async saveProactiveDebugInfo(step: number): Promise<{ screenshotPath?: string; htmlPath?: string }> {
|
private async saveProactiveDebugInfo(step: number): Promise<{ screenshotPath?: string; htmlPath?: string; domPath?: string }> {
|
||||||
if (!this.page) return {};
|
if (!this.page) return {};
|
||||||
|
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
const baseName = `debug-before-step-${step}-${timestamp}`;
|
const baseName = `debug-before-step-${step}-${timestamp}`;
|
||||||
const debugDir = path.join(process.cwd(), 'debug-screenshots');
|
const debugDir = path.join(process.cwd(), 'debug-screenshots');
|
||||||
const result: { screenshotPath?: string; htmlPath?: string } = {};
|
const result: { screenshotPath?: string; htmlPath?: string; domPath?: string } = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.promises.mkdir(debugDir, { recursive: true });
|
await fs.promises.mkdir(debugDir, { recursive: true });
|
||||||
@@ -972,6 +1005,22 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
|
|||||||
await fs.promises.writeFile(htmlPath, html);
|
await fs.promises.writeFile(htmlPath, html);
|
||||||
result.htmlPath = htmlPath;
|
result.htmlPath = htmlPath;
|
||||||
this.log('info', `Pre-step HTML saved: ${htmlPath}`, { path: htmlPath, step });
|
this.log('info', `Pre-step HTML saved: ${htmlPath}`, { path: htmlPath, step });
|
||||||
|
|
||||||
|
// Save structural DOM export alongside HTML
|
||||||
|
try {
|
||||||
|
await this.page.evaluate(() => {
|
||||||
|
(window as any).__name = (window as any).__name || ((fn: any) => fn);
|
||||||
|
});
|
||||||
|
const items = (await this.page.evaluate(
|
||||||
|
extractDom as () => ExportedElement[]
|
||||||
|
)) as unknown as ExportedElement[];
|
||||||
|
const domPath = path.join(debugDir, `${baseName}.dom.json`);
|
||||||
|
await fs.promises.writeFile(domPath, JSON.stringify(items, null, 2), 'utf8');
|
||||||
|
result.domPath = domPath;
|
||||||
|
this.log('info', `Pre-step DOM saved: ${domPath}`, { path: domPath, step });
|
||||||
|
} catch (domErr) {
|
||||||
|
this.log('warn', 'Failed to save proactive debug DOM', { error: String(domErr), step });
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Don't fail step execution if debug save fails
|
// Don't fail step execution if debug save fails
|
||||||
this.log('warn', 'Failed to save proactive debug info', { error: String(e), step });
|
this.log('warn', 'Failed to save proactive debug info', { error: String(e), step });
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { PlaywrightBrowserSession } from './PlaywrightBrowserSession';
|
|||||||
import { IRacingDomNavigator } from '../dom/IRacingDomNavigator';
|
import { IRacingDomNavigator } from '../dom/IRacingDomNavigator';
|
||||||
import { IRacingDomInteractor } from '../dom/IRacingDomInteractor';
|
import { IRacingDomInteractor } from '../dom/IRacingDomInteractor';
|
||||||
import { IRACING_SELECTORS } from '../dom/IRacingSelectors';
|
import { IRACING_SELECTORS } from '../dom/IRacingSelectors';
|
||||||
|
import { getFixtureForStep } from '../engine/FixtureServer';
|
||||||
import type {
|
import type {
|
||||||
PageStateValidation,
|
PageStateValidation,
|
||||||
PageStateValidationResult,
|
PageStateValidationResult,
|
||||||
@@ -306,6 +307,14 @@ export class WizardStepOrchestrator {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
|
if (!this.isRealMode() && this.config.baseUrl) {
|
||||||
|
const fixture = getFixtureForStep(2);
|
||||||
|
if (fixture) {
|
||||||
|
const base = this.config.baseUrl.replace(/\/$/, '');
|
||||||
|
await this.navigator.navigateToPage(`${base}/${fixture}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
await this.clickAction('create');
|
await this.clickAction('create');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ import { permissionGuard, shouldSkipRealAutomationTests } from './support/Permis
|
|||||||
const IRACING_URL = 'https://members-ng.iracing.com/web/racing/hosted/browse-sessions';
|
const IRACING_URL = 'https://members-ng.iracing.com/web/racing/hosted/browse-sessions';
|
||||||
const WINDOW_TITLE_PATTERN = 'iRacing';
|
const WINDOW_TITLE_PATTERN = 'iRacing';
|
||||||
|
|
||||||
|
const RUN_REAL_AUTOMATION_SMOKE = process.env.RUN_REAL_AUTOMATION_SMOKE === '1';
|
||||||
|
const describeSmoke = RUN_REAL_AUTOMATION_SMOKE ? describe : describe.skip;
|
||||||
|
|
||||||
let skipReason: string | null = null;
|
let skipReason: string | null = null;
|
||||||
let browserProcess: ChildProcess | null = null;
|
let browserProcess: ChildProcess | null = null;
|
||||||
|
|
||||||
@@ -85,7 +88,7 @@ async function closeBrowser(): Promise<void> {
|
|||||||
browserProcess = null;
|
browserProcess = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('E2E Real Automation Tests - REAL iRacing Website', () => {
|
describeSmoke('Real automation smoke – REAL iRacing Website', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
// Check permissions first
|
// Check permissions first
|
||||||
skipReason = await shouldSkipRealAutomationTests() ?? null;
|
skipReason = await shouldSkipRealAutomationTests() ?? null;
|
||||||
|
|||||||
153
tests/e2e/workflows/full-hosted-session.workflow.e2e.test.ts
Normal file
153
tests/e2e/workflows/full-hosted-session.workflow.e2e.test.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||||
|
import {
|
||||||
|
PlaywrightAutomationAdapter,
|
||||||
|
FixtureServer,
|
||||||
|
} from 'packages/infrastructure/adapters/automation';
|
||||||
|
import { InMemorySessionRepository } from 'packages/infrastructure/repositories/InMemorySessionRepository';
|
||||||
|
import { MockAutomationEngineAdapter } from 'packages/infrastructure/adapters/automation/engine/MockAutomationEngineAdapter';
|
||||||
|
import { MockBrowserAutomationAdapter } from 'packages/infrastructure/adapters/automation/engine/MockBrowserAutomationAdapter';
|
||||||
|
import { StartAutomationSessionUseCase } from 'packages/application/use-cases/StartAutomationSessionUseCase';
|
||||||
|
import { StepId } from 'packages/domain/value-objects/StepId';
|
||||||
|
|
||||||
|
describe('Workflow – hosted session end-to-end (fixture-backed)', () => {
|
||||||
|
let server: FixtureServer;
|
||||||
|
let adapter: PlaywrightAutomationAdapter;
|
||||||
|
let baseUrl: string;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
server = new FixtureServer();
|
||||||
|
const info = await server.start();
|
||||||
|
baseUrl = info.url;
|
||||||
|
|
||||||
|
adapter = new PlaywrightAutomationAdapter(
|
||||||
|
{
|
||||||
|
headless: true,
|
||||||
|
timeout: 10_000,
|
||||||
|
baseUrl,
|
||||||
|
mode: 'mock',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const connectResult = await adapter.connect();
|
||||||
|
expect(connectResult.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await adapter.disconnect();
|
||||||
|
await server.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
function createFixtureEngine() {
|
||||||
|
const repository = new InMemorySessionRepository();
|
||||||
|
const engine = new MockAutomationEngineAdapter(adapter, repository);
|
||||||
|
const useCase = new StartAutomationSessionUseCase(engine, adapter, repository);
|
||||||
|
return { repository, engine, useCase };
|
||||||
|
}
|
||||||
|
|
||||||
|
it('runs 1–18 from use case to STOPPED_AT_STEP_18', async () => {
|
||||||
|
const { repository, engine, useCase } = createFixtureEngine();
|
||||||
|
|
||||||
|
const config: any = {
|
||||||
|
sessionName: 'Fixture E2E – full workflow',
|
||||||
|
trackId: 'spa',
|
||||||
|
carIds: ['dallara-f3'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const dto = await useCase.execute(config);
|
||||||
|
|
||||||
|
expect(dto.state).toBe('PENDING');
|
||||||
|
expect(dto.currentStep).toBe(1);
|
||||||
|
|
||||||
|
await engine.executeStep(StepId.create(1), config);
|
||||||
|
|
||||||
|
const deadline = Date.now() + 60_000;
|
||||||
|
let finalSession = null;
|
||||||
|
|
||||||
|
// Poll repository until automation loop completes
|
||||||
|
// MockAutomationEngineAdapter drives the step orchestrator internally.
|
||||||
|
// Session should end in STOPPED_AT_STEP_18 when step 18 completes.
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
const sessions = await repository.findAll();
|
||||||
|
finalSession = sessions[0] ?? null;
|
||||||
|
|
||||||
|
if (finalSession && (finalSession.state.isStoppedAtStep18() || finalSession.state.isFailed())) {
|
||||||
|
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(18);
|
||||||
|
expect(finalSession!.startedAt).toBeInstanceOf(Date);
|
||||||
|
expect(finalSession!.completedAt).toBeInstanceOf(Date);
|
||||||
|
expect(finalSession!.errorMessage).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks session as FAILED on mid-flow automation error with diagnostics', async () => {
|
||||||
|
const repository = new InMemorySessionRepository();
|
||||||
|
const failingAdapter = new MockBrowserAutomationAdapter({
|
||||||
|
simulateFailures: true,
|
||||||
|
failureRate: 1.0,
|
||||||
|
});
|
||||||
|
await failingAdapter.connect();
|
||||||
|
|
||||||
|
const engine = new MockAutomationEngineAdapter(
|
||||||
|
failingAdapter as any,
|
||||||
|
repository,
|
||||||
|
);
|
||||||
|
const useCase = new StartAutomationSessionUseCase(
|
||||||
|
engine,
|
||||||
|
failingAdapter as any,
|
||||||
|
repository,
|
||||||
|
);
|
||||||
|
|
||||||
|
const config: any = {
|
||||||
|
sessionName: 'Fixture E2E – failure workflow',
|
||||||
|
trackId: 'spa',
|
||||||
|
carIds: ['dallara-f3'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const dto = await useCase.execute(config);
|
||||||
|
|
||||||
|
expect(dto.state).toBe('PENDING');
|
||||||
|
expect(dto.currentStep).toBe(1);
|
||||||
|
|
||||||
|
await engine.executeStep(StepId.create(1), config);
|
||||||
|
|
||||||
|
const deadline = Date.now() + 30_000;
|
||||||
|
let finalSession = null;
|
||||||
|
|
||||||
|
// Poll for failure state
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
const sessions = await repository.findAll();
|
||||||
|
finalSession = sessions[0] ?? null;
|
||||||
|
|
||||||
|
if (finalSession && (finalSession.state.isFailed() || finalSession.state.isStoppedAtStep18())) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.now() > deadline) {
|
||||||
|
throw new Error('Timed out waiting for automation workflow to fail');
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
await failingAdapter.disconnect();
|
||||||
|
|
||||||
|
expect(finalSession).not.toBeNull();
|
||||||
|
expect(
|
||||||
|
finalSession!.state.isFailed() || finalSession!.state.isStoppedAtStep18(),
|
||||||
|
).toBe(true);
|
||||||
|
if (finalSession!.state.isFailed()) {
|
||||||
|
expect(finalSession!.errorMessage).toBeDefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,28 +1,35 @@
|
|||||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||||
import path from 'path';
|
import { PlaywrightAutomationAdapter, FixtureServer } from 'packages/infrastructure/adapters/automation';
|
||||||
import { PlaywrightAutomationAdapter } from 'packages/infrastructure/adapters/automation';
|
|
||||||
import { StepId } from 'packages/domain/value-objects/StepId';
|
import { StepId } from 'packages/domain/value-objects/StepId';
|
||||||
|
|
||||||
describe('Workflow – steps 7–9 cars flow', () => {
|
describe('Workflow – steps 7–9 cars flow (fixture-backed)', () => {
|
||||||
let adapter: PlaywrightAutomationAdapter;
|
let adapter: PlaywrightAutomationAdapter;
|
||||||
const fixtureBaseUrl = `file://${path.resolve(process.cwd(), 'html-dumps')}`;
|
let server: FixtureServer;
|
||||||
|
let baseUrl: string;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
adapter = new PlaywrightAutomationAdapter({
|
server = new FixtureServer();
|
||||||
headless: true,
|
const info = await server.start();
|
||||||
timeout: 5000,
|
baseUrl = info.url;
|
||||||
baseUrl: fixtureBaseUrl,
|
|
||||||
mode: 'mock',
|
adapter = new PlaywrightAutomationAdapter(
|
||||||
});
|
{
|
||||||
|
headless: true,
|
||||||
|
timeout: 5000,
|
||||||
|
baseUrl,
|
||||||
|
mode: 'mock',
|
||||||
|
}
|
||||||
|
);
|
||||||
await adapter.connect();
|
await adapter.connect();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await adapter.disconnect();
|
await adapter.disconnect();
|
||||||
|
await server.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('executes time limits, cars, and add car in sequence', async () => {
|
it('executes time limits, cars, and add car in sequence using fixtures', async () => {
|
||||||
await adapter.navigateToPage(`${fixtureBaseUrl}/step-07-time-limits.html`);
|
await adapter.navigateToPage(server.getFixtureUrl(7));
|
||||||
const step7Result = await adapter.executeStep(StepId.create(7), {
|
const step7Result = await adapter.executeStep(StepId.create(7), {
|
||||||
practice: 10,
|
practice: 10,
|
||||||
qualify: 10,
|
qualify: 10,
|
||||||
@@ -30,11 +37,11 @@ describe('Workflow – steps 7–9 cars flow', () => {
|
|||||||
});
|
});
|
||||||
expect(step7Result.success).toBe(true);
|
expect(step7Result.success).toBe(true);
|
||||||
|
|
||||||
await adapter.navigateToPage(`${fixtureBaseUrl}/step-08-set-cars.html`);
|
await adapter.navigateToPage(server.getFixtureUrl(8));
|
||||||
const step8Result = await adapter.executeStep(StepId.create(8), {});
|
const step8Result = await adapter.executeStep(StepId.create(8), {});
|
||||||
expect(step8Result.success).toBe(true);
|
expect(step8Result.success).toBe(true);
|
||||||
|
|
||||||
await adapter.navigateToPage(`${fixtureBaseUrl}/step-09-add-car.html`);
|
await adapter.navigateToPage(server.getFixtureUrl(9));
|
||||||
const step9Result = await adapter.executeStep(StepId.create(9), {
|
const step9Result = await adapter.executeStep(StepId.create(9), {
|
||||||
carSearch: 'Porsche 911 GT3 R',
|
carSearch: 'Porsche 911 GT3 R',
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user