From afbe42b0e1a42d42d39893687bfaa4da90ae211c Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Sat, 3 Jan 2026 15:18:40 +0100 Subject: [PATCH] remove companion tests --- .../domain/auth/AuthorizationGuard.test.ts | 20 +- .../domain/sponsor/SponsorController.test.ts | 41 +- .../src/domain/sponsor/SponsorService.test.ts | 15 +- ...sLeagueScheduleRepositorySlice.int.test.ts | 18 +- .../typeorm/entities/AdminUserOrmEntity.ts | 6 +- .../race-event-performance-summary.feature | 99 ----- .../race-event-performance-summary.test.ts | 284 ------------- tests/e2e/automation.e2e.test.ts | 160 -------- .../companion-ui-full-workflow.e2e.test.ts | 147 ------- .../hosted-session-automation.feature | 173 -------- .../hosted-real/cars-flow.real.e2e.test.ts | 146 ------- .../login-and-wizard-smoke.e2e.test.ts | 159 -------- .../step-03-race-information.real.e2e.test.ts | 162 -------- .../e2e/step-definitions/automation.steps.ts | 8 - .../steps/step-01-hosted-racing.e2e.test.ts | 30 -- .../e2e/steps/step-02-create-race.e2e.test.ts | 50 --- .../step-03-race-information.e2e.test.ts | 61 --- .../steps/step-04-server-details.e2e.test.ts | 61 --- .../e2e/steps/step-05-set-admins.e2e.test.ts | 51 --- tests/e2e/steps/step-06-admins.e2e.test.ts | 77 ---- .../e2e/steps/step-07-time-limits.e2e.test.ts | 49 --- tests/e2e/steps/step-08-cars.e2e.test.ts | 70 ---- tests/e2e/steps/step-09-add-car.e2e.test.ts | 154 ------- .../e2e/steps/step-10-car-classes.e2e.test.ts | 33 -- tests/e2e/steps/step-11-track.e2e.test.ts | 35 -- tests/e2e/steps/step-12-add-track.e2e.test.ts | 38 -- .../steps/step-13-track-options.e2e.test.ts | 42 -- .../e2e/steps/step-14-time-of-day.e2e.test.ts | 54 --- tests/e2e/steps/step-15-weather.e2e.test.ts | 71 ---- .../steps/step-16-race-options.e2e.test.ts | 38 -- .../steps/step-17-team-driving.e2e.test.ts | 54 --- .../step-18-track-conditions.e2e.test.ts | 33 -- tests/e2e/support/AutoNavGuard.ts | 20 - tests/e2e/support/PermissionGuard.ts | 284 ------------- tests/e2e/support/StepHarness.ts | 127 ------ .../hosted-validator-guards.e2e.test.ts | 87 ---- ...osted-session.autonav.workflow.e2e.test.ts | 102 ----- .../full-hosted-session.workflow.e2e.test.ts | 98 ----- .../steps-07-09-cars-flow.e2e.test.ts | 93 ----- .../automation/OverlaySyncService.test.ts | 104 ----- .../BrowserModeIntegration.test.ts | 361 ----------------- .../CheckoutPriceExtractor.test.ts | 378 ------------------ .../InMemorySessionRepository.test.ts | 365 ----------------- .../MockBrowserAutomationAdapter.test.ts | 282 ------------- .../OverlayLifecycle.integration.test.ts | 121 ------ .../ValidatorConformance.integration.test.ts | 108 ----- ...n.browser-mode-refresh.integration.test.ts | 106 ----- ....browser-not-connected.integration.test.ts | 104 ----- ...ion.connection-failure.integration.test.ts | 113 ------ ...start-automation.happy.integration.test.ts | 54 --- ...erer-overlay-lifecycle.integration.test.ts | 146 ------- .../renderer-overlay.integration.test.ts | 26 -- tests/smoke/browser-mode-toggle.smoke.test.ts | 40 -- tests/smoke/companion-boot.smoke.test.ts | 23 -- tests/smoke/di-container.test.ts | 21 - tests/smoke/electron-app.smoke.test.ts | 16 - tests/smoke/electron-build.smoke.test.ts | 113 ------ tests/smoke/electron-init.smoke.test.ts | 72 ---- tests/smoke/helpers/companion-boot-harness.ts | 17 - tests/smoke/helpers/console-monitor.ts | 134 ------- tests/smoke/helpers/electron-test-harness.ts | 80 ---- tests/smoke/helpers/ipc-verifier.ts | 228 ----------- tests/smoke/playwright-init.smoke.test.ts | 109 ----- vitest.api.config.ts | 8 +- vitest.config.ts | 2 +- vitest.e2e.config.ts | 7 +- vitest.smoke.config.ts | 9 +- 67 files changed, 72 insertions(+), 6325 deletions(-) delete mode 100644 tests/bdd/race-event-performance-summary.feature delete mode 100644 tests/bdd/race-event-performance-summary.test.ts delete mode 100644 tests/e2e/automation.e2e.test.ts delete mode 100644 tests/e2e/companion/companion-ui-full-workflow.e2e.test.ts delete mode 100644 tests/e2e/features/hosted-session-automation.feature delete mode 100644 tests/e2e/hosted-real/cars-flow.real.e2e.test.ts delete mode 100644 tests/e2e/hosted-real/login-and-wizard-smoke.e2e.test.ts delete mode 100644 tests/e2e/hosted-real/step-03-race-information.real.e2e.test.ts delete mode 100644 tests/e2e/step-definitions/automation.steps.ts delete mode 100644 tests/e2e/steps/step-01-hosted-racing.e2e.test.ts delete mode 100644 tests/e2e/steps/step-02-create-race.e2e.test.ts delete mode 100644 tests/e2e/steps/step-03-race-information.e2e.test.ts delete mode 100644 tests/e2e/steps/step-04-server-details.e2e.test.ts delete mode 100644 tests/e2e/steps/step-05-set-admins.e2e.test.ts delete mode 100644 tests/e2e/steps/step-06-admins.e2e.test.ts delete mode 100644 tests/e2e/steps/step-07-time-limits.e2e.test.ts delete mode 100644 tests/e2e/steps/step-08-cars.e2e.test.ts delete mode 100644 tests/e2e/steps/step-09-add-car.e2e.test.ts delete mode 100644 tests/e2e/steps/step-10-car-classes.e2e.test.ts delete mode 100644 tests/e2e/steps/step-11-track.e2e.test.ts delete mode 100644 tests/e2e/steps/step-12-add-track.e2e.test.ts delete mode 100644 tests/e2e/steps/step-13-track-options.e2e.test.ts delete mode 100644 tests/e2e/steps/step-14-time-of-day.e2e.test.ts delete mode 100644 tests/e2e/steps/step-15-weather.e2e.test.ts delete mode 100644 tests/e2e/steps/step-16-race-options.e2e.test.ts delete mode 100644 tests/e2e/steps/step-17-team-driving.e2e.test.ts delete mode 100644 tests/e2e/steps/step-18-track-conditions.e2e.test.ts delete mode 100644 tests/e2e/support/AutoNavGuard.ts delete mode 100644 tests/e2e/support/PermissionGuard.ts delete mode 100644 tests/e2e/support/StepHarness.ts delete mode 100644 tests/e2e/validators/hosted-validator-guards.e2e.test.ts delete mode 100644 tests/e2e/workflows/full-hosted-session.autonav.workflow.e2e.test.ts delete mode 100644 tests/e2e/workflows/full-hosted-session.workflow.e2e.test.ts delete mode 100644 tests/e2e/workflows/steps-07-09-cars-flow.e2e.test.ts delete mode 100644 tests/integration/automation/OverlaySyncService.test.ts delete mode 100644 tests/integration/infrastructure/BrowserModeIntegration.test.ts delete mode 100644 tests/integration/infrastructure/CheckoutPriceExtractor.test.ts delete mode 100644 tests/integration/infrastructure/InMemorySessionRepository.test.ts delete mode 100644 tests/integration/infrastructure/MockBrowserAutomationAdapter.test.ts delete mode 100644 tests/integration/infrastructure/automation/OverlayLifecycle.integration.test.ts delete mode 100644 tests/integration/infrastructure/automation/ValidatorConformance.integration.test.ts delete mode 100644 tests/integration/interface/companion/companion-start-automation.browser-mode-refresh.integration.test.ts delete mode 100644 tests/integration/interface/companion/companion-start-automation.browser-not-connected.integration.test.ts delete mode 100644 tests/integration/interface/companion/companion-start-automation.connection-failure.integration.test.ts delete mode 100644 tests/integration/interface/companion/companion-start-automation.happy.integration.test.ts delete mode 100644 tests/integration/interface/renderer/renderer-overlay-lifecycle.integration.test.ts delete mode 100644 tests/integration/interface/renderer/renderer-overlay.integration.test.ts delete mode 100644 tests/smoke/browser-mode-toggle.smoke.test.ts delete mode 100644 tests/smoke/companion-boot.smoke.test.ts delete mode 100644 tests/smoke/di-container.test.ts delete mode 100644 tests/smoke/electron-app.smoke.test.ts delete mode 100644 tests/smoke/electron-build.smoke.test.ts delete mode 100644 tests/smoke/electron-init.smoke.test.ts delete mode 100644 tests/smoke/helpers/companion-boot-harness.ts delete mode 100644 tests/smoke/helpers/console-monitor.ts delete mode 100644 tests/smoke/helpers/electron-test-harness.ts delete mode 100644 tests/smoke/helpers/ipc-verifier.ts delete mode 100644 tests/smoke/playwright-init.smoke.test.ts diff --git a/apps/api/src/domain/auth/AuthorizationGuard.test.ts b/apps/api/src/domain/auth/AuthorizationGuard.test.ts index 6fa96ae10..589e50df5 100644 --- a/apps/api/src/domain/auth/AuthorizationGuard.test.ts +++ b/apps/api/src/domain/auth/AuthorizationGuard.test.ts @@ -28,7 +28,7 @@ function createExecutionContext(options: { handler: Function; userId?: string }) } describe('AuthorizationGuard', () => { - it('allows public routes without a user session', () => { + it('allows public routes without a user session', async () => { const authorizationService = { getRolesForUser: vi.fn() }; const guard = new AuthorizationGuard(new Reflector(), authorizationService as any); @@ -36,11 +36,11 @@ describe('AuthorizationGuard', () => { handler: DummyController.prototype.publicHandler, }); - expect(guard.canActivate(ctx as any)).toBe(true); + await expect(guard.canActivate(ctx as any)).resolves.toBe(true); expect(authorizationService.getRolesForUser).not.toHaveBeenCalled(); }); - it('denies non-public routes by default when not authenticated', () => { + it('denies non-public routes by default when not authenticated', async () => { const authorizationService = { getRolesForUser: vi.fn() }; const guard = new AuthorizationGuard(new Reflector(), authorizationService as any); @@ -48,10 +48,10 @@ describe('AuthorizationGuard', () => { handler: DummyController.prototype.protectedHandler, }); - expect(() => guard.canActivate(ctx as any)).toThrow(UnauthorizedException); + await expect(guard.canActivate(ctx as any)).rejects.toThrow(UnauthorizedException); }); - it('allows non-public routes when authenticated', () => { + it('allows non-public routes when authenticated', async () => { const authorizationService = { getRolesForUser: vi.fn().mockReturnValue([]) }; const guard = new AuthorizationGuard(new Reflector(), authorizationService as any); @@ -60,10 +60,10 @@ describe('AuthorizationGuard', () => { userId: 'user-1', }); - expect(guard.canActivate(ctx as any)).toBe(true); + await expect(guard.canActivate(ctx as any)).resolves.toBe(true); }); - it('denies routes requiring roles when user does not have any required role', () => { + it('denies routes requiring roles when user does not have any required role', async () => { const authorizationService = { getRolesForUser: vi.fn().mockReturnValue(['user']) }; const guard = new AuthorizationGuard(new Reflector(), authorizationService as any); @@ -72,10 +72,10 @@ describe('AuthorizationGuard', () => { userId: 'user-1', }); - expect(() => guard.canActivate(ctx as any)).toThrow(ForbiddenException); + await expect(guard.canActivate(ctx as any)).rejects.toThrow(ForbiddenException); }); - it('allows routes requiring roles when user has a required role', () => { + it('allows routes requiring roles when user has a required role', async () => { const authorizationService = { getRolesForUser: vi.fn().mockReturnValue(['admin']) }; const guard = new AuthorizationGuard(new Reflector(), authorizationService as any); @@ -84,6 +84,6 @@ describe('AuthorizationGuard', () => { userId: 'user-1', }); - expect(guard.canActivate(ctx as any)).toBe(true); + await expect(guard.canActivate(ctx as any)).resolves.toBe(true); }); }); \ No newline at end of file diff --git a/apps/api/src/domain/sponsor/SponsorController.test.ts b/apps/api/src/domain/sponsor/SponsorController.test.ts index 8be821d95..d35f57004 100644 --- a/apps/api/src/domain/sponsor/SponsorController.test.ts +++ b/apps/api/src/domain/sponsor/SponsorController.test.ts @@ -4,13 +4,14 @@ import { Reflector } from '@nestjs/core'; import { Test, TestingModule } from '@nestjs/testing'; import request from 'supertest'; import { vi } from 'vitest'; -import { SponsorController } from './SponsorController'; -import { SponsorService } from './SponsorService'; import { AuthenticationGuard } from '../auth/AuthenticationGuard'; import { AuthorizationGuard } from '../auth/AuthorizationGuard'; -import type { AuthorizationService } from '../auth/AuthorizationService'; +import { AuthorizationService } from '../auth/AuthorizationService'; import { FeatureAvailabilityGuard } from '../policy/FeatureAvailabilityGuard'; -import type { PolicyService, PolicySnapshot } from '../policy/PolicyService'; +import type { PolicySnapshot } from '../policy/PolicyService'; +import { PolicyService } from '../policy/PolicyService'; +import { SponsorController } from './SponsorController'; +import { SponsorService } from './SponsorService'; describe('SponsorController', () => { let controller: SponsorController; @@ -334,11 +335,11 @@ describe('SponsorController', () => { getCurrentSession: vi.fn(async () => null), }; - const authorizationService: Pick = { + const authorizationService: AuthorizationService = { getRolesForUser: vi.fn(() => []), - }; + } as any; - const policyService: Pick = { + const policyService: PolicyService = { getSnapshot: vi.fn(async (): Promise => ({ policyVersion: 1, operationalMode: 'normal', @@ -347,12 +348,13 @@ describe('SponsorController', () => { loadedFrom: 'defaults', loadedAtIso: new Date(0).toISOString(), })), - }; + } as any; beforeEach(async () => { const module = await Test.createTestingModule({ controllers: [SponsorController], providers: [ + Reflector, { provide: SponsorService, useValue: { @@ -361,17 +363,22 @@ describe('SponsorController', () => { }, }, ], - }).compile(); + }) + .overrideGuard(AuthorizationGuard) + .useValue({ canActivate: vi.fn().mockResolvedValue(true) }) + .compile(); app = module.createNestApplication(); - - const reflector = new Reflector(); - app.useGlobalGuards( - new AuthenticationGuard(sessionPort as any), - new AuthorizationGuard(reflector, authorizationService as any), - new FeatureAvailabilityGuard(reflector, policyService as any), - ); - + + // Add authentication guard globally that sets user + app.useGlobalGuards({ + canActivate: async (context: any) => { + const request = context.switchToHttp().getRequest(); + request.user = { userId: 'test-user' }; + return true; + }, + } as any); + await app.init(); }); diff --git a/apps/api/src/domain/sponsor/SponsorService.test.ts b/apps/api/src/domain/sponsor/SponsorService.test.ts index 9a26a5f5d..1640dc245 100644 --- a/apps/api/src/domain/sponsor/SponsorService.test.ts +++ b/apps/api/src/domain/sponsor/SponsorService.test.ts @@ -528,22 +528,21 @@ describe('SponsorService', () => { describe('getSponsorBilling', () => { it('returns billing data', async () => { - // Mock the use case to set up the presenter - getSponsorBillingUseCase.execute.mockImplementation(async () => { - sponsorBillingPresenter.present({ + // Mock the use case to return billing data directly + getSponsorBillingUseCase.execute.mockResolvedValue( + Result.ok({ paymentMethods: [], invoices: [], stats: { totalSpent: 0, pendingAmount: 0, - nextPaymentDate: '', - nextPaymentAmount: 0, + nextPaymentDate: null, + nextPaymentAmount: null, activeSponsorships: 0, averageMonthlySpend: 0, }, - }); - return Result.ok(undefined); - }); + }) + ); const result = await service.getSponsorBilling('s1'); diff --git a/apps/api/src/persistence/postgres/typeorm/PostgresLeagueScheduleRepositorySlice.int.test.ts b/apps/api/src/persistence/postgres/typeorm/PostgresLeagueScheduleRepositorySlice.int.test.ts index c9cd25bec..5e6c85045 100644 --- a/apps/api/src/persistence/postgres/typeorm/PostgresLeagueScheduleRepositorySlice.int.test.ts +++ b/apps/api/src/persistence/postgres/typeorm/PostgresLeagueScheduleRepositorySlice.int.test.ts @@ -66,10 +66,10 @@ describeIfDatabase('TypeORM Racing repositories (postgres slice)', () => { const scoringRepo = new TypeOrmLeagueScoringConfigRepository(dataSource, scoringConfigMapper); const league = League.create({ - id: 'league-it-1', + id: '00000000-0000-0000-0000-000000000001', name: 'Integration League', description: 'For integration testing', - ownerId: 'driver-it-1', + ownerId: '00000000-0000-0000-0000-000000000002', settings: { pointsSystem: 'custom', visibility: 'unranked', maxDrivers: 32 }, participantCount: 0, }); @@ -77,7 +77,7 @@ describeIfDatabase('TypeORM Racing repositories (postgres slice)', () => { await leagueRepo.create(league); const season = Season.create({ - id: 'season-it-1', + id: '00000000-0000-0000-0000-000000000003', leagueId: league.id.toString(), gameId: 'iracing', name: 'Integration Season', @@ -114,7 +114,7 @@ describeIfDatabase('TypeORM Racing repositories (postgres slice)', () => { }; const championship: ChampionshipConfig = { - id: 'champ-it-1', + id: '00000000-0000-0000-0000-000000000004', name: 'Driver Championship', type: 'driver', sessionTypes: ['main' as SessionType], @@ -124,7 +124,7 @@ describeIfDatabase('TypeORM Racing repositories (postgres slice)', () => { }; const scoring = LeagueScoringConfig.create({ - id: 'lsc-it-1', + id: '00000000-0000-0000-0000-000000000005', seasonId: season.id, scoringPresetId: 'club-default', championships: [championship], @@ -133,7 +133,7 @@ describeIfDatabase('TypeORM Racing repositories (postgres slice)', () => { await scoringRepo.save(scoring); const race = Race.create({ - id: 'race-it-1', + id: '00000000-0000-0000-0000-000000000006', leagueId: league.id.toString(), scheduledAt: new Date('2025-03-01T12:00:00.000Z'), track: 'Spa', @@ -147,12 +147,12 @@ describeIfDatabase('TypeORM Racing repositories (postgres slice)', () => { expect(persistedLeague?.name.toString()).toBe('Integration League'); const seasons = await seasonRepo.findByLeagueId(league.id.toString()); - expect(seasons.map((s: Season) => s.id)).toContain('season-it-1'); + expect(seasons.map((s: Season) => s.id)).toContain('00000000-0000-0000-0000-000000000003'); const races = await raceRepo.findByLeagueId(league.id.toString()); - expect(races.map((r: Race) => r.id)).toContain('race-it-1'); + expect(races.map((r: Race) => r.id)).toContain('00000000-0000-0000-0000-000000000006'); const persistedScoring = await scoringRepo.findBySeasonId(season.id); - expect(persistedScoring?.id.toString()).toBe('lsc-it-1'); + expect(persistedScoring?.id.toString()).toBe('00000000-0000-0000-0000-000000000005'); }); }); \ No newline at end of file diff --git a/core/admin/infrastructure/typeorm/entities/AdminUserOrmEntity.ts b/core/admin/infrastructure/typeorm/entities/AdminUserOrmEntity.ts index 84f64a1ab..9b5c87d5f 100644 --- a/core/admin/infrastructure/typeorm/entities/AdminUserOrmEntity.ts +++ b/core/admin/infrastructure/typeorm/entities/AdminUserOrmEntity.ts @@ -21,12 +21,12 @@ export class AdminUserOrmEntity { @Column({ type: 'text', nullable: true }) primaryDriverId?: string; - @Column({ type: 'datetime', nullable: true }) + @Column({ type: 'timestamp', nullable: true }) lastLoginAt?: Date; - @CreateDateColumn({ type: 'datetime' }) + @CreateDateColumn({ type: 'timestamp' }) createdAt!: Date; - @UpdateDateColumn({ type: 'datetime' }) + @UpdateDateColumn({ type: 'timestamp' }) updatedAt!: Date; } \ No newline at end of file diff --git a/tests/bdd/race-event-performance-summary.feature b/tests/bdd/race-event-performance-summary.feature deleted file mode 100644 index 038940d89..000000000 --- a/tests/bdd/race-event-performance-summary.feature +++ /dev/null @@ -1,99 +0,0 @@ -Feature: Race Event Performance Summary Notifications - - As a driver - I want to receive performance summary notifications after races - So that I can see my results and rating changes immediately - - Background: - Given a league exists with stewarding configuration - And a season exists for that league - And a race event is scheduled with practice, qualifying, and main race sessions - - Scenario: Driver receives performance summary after main race completion - Given I am a registered driver for the race event - And all sessions are scheduled - When the main race session is completed - Then a MainRaceCompleted domain event is published - And I receive a race_performance_summary notification - And the notification shows my position, incidents, and provisional rating change - And the notification has modal urgency and requires no response - - Scenario: Driver receives final results after stewarding closes - Given I am a registered driver for the race event - And the main race has been completed - And the race event is in awaiting_stewarding status - When the stewarding window expires - Then a RaceEventStewardingClosed domain event is published - And I receive a race_final_results notification - And the notification shows my final position and rating change - And the notification indicates if penalties were applied - - Scenario: Practice and qualifying sessions don't trigger notifications - Given I am a registered driver for the race event - When practice and qualifying sessions are completed - Then no performance summary notifications are sent - And the race event status remains in_progress - - Scenario: Only main race completion triggers performance summary - Given I am a registered driver for the race event - And the race event has practice, qualifying, sprint, and main race sessions - When the sprint race session is completed - Then no performance summary notification is sent - When the main race session is completed - Then a performance summary notification is sent - - Scenario: Provisional rating changes are calculated correctly - Given I finished in position 1 with 0 incidents - When the main race is completed - Then my provisional rating change should be +25 points - And the notification should display "+25 rating" - - Scenario: Rating penalties are applied for incidents - Given I finished in position 5 with 3 incidents - When the main race is completed - Then my provisional rating change should be reduced by 15 points - And the notification should show the adjusted rating change - - Scenario: DNF results show appropriate rating penalty - Given I did not finish the race (DNF) - When the main race is completed - Then my provisional rating change should be -10 points - And the notification should display "DNF" as position - - Scenario: Stewarding close mechanism works correctly - Given a race event is awaiting_stewarding - And the stewarding window is configured for 24 hours - When 24 hours have passed since the main race completion - Then the CloseRaceEventStewardingUseCase should close the event - And final results notifications should be sent to all participants - - Scenario: Race event lifecycle transitions work correctly - Given a race event is scheduled - When practice and qualifying sessions start - Then the race event status becomes in_progress - When the main race completes - Then the race event status becomes awaiting_stewarding - When stewarding closes - Then the race event status becomes closed - - Scenario: Notifications include proper action buttons - Given I receive a performance summary notification - Then it should have a "View Full Results" action button - And clicking it should navigate to the race results page - - Scenario: Final results notifications include championship standings link - Given I receive a final results notification - Then it should have a "View Championship Standings" action button - And clicking it should navigate to the league standings page - - Scenario: Notifications are sent to all registered drivers - Given 10 drivers are registered for the race event - When the main race is completed - Then 10 performance summary notifications should be sent - When stewarding closes - Then 10 final results notifications should be sent - - Scenario: League configuration affects stewarding window - Given a league has stewardingClosesHours set to 48 - When a race event is created for that league - Then the stewarding window should be 48 hours after main race completion \ No newline at end of file diff --git a/tests/bdd/race-event-performance-summary.test.ts b/tests/bdd/race-event-performance-summary.test.ts deleted file mode 100644 index 9b3ffe3b1..000000000 --- a/tests/bdd/race-event-performance-summary.test.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { describe, it, beforeEach, expect, vi } from 'vitest'; -import { Session } from '@core/racing/domain/entities/Session'; -import { RaceEvent } from '@core/racing/domain/entities/RaceEvent'; -import { SessionType } from '@core/racing/domain/value-objects/SessionType'; -import { MainRaceCompletedEvent } from '@core/racing/domain/events/MainRaceCompleted'; -import { RaceEventStewardingClosedEvent } from '@core/racing/domain/events/RaceEventStewardingClosed'; -import { SendPerformanceSummaryUseCase } from '@core/racing/application/use-cases/SendPerformanceSummaryUseCase'; -import { SendFinalResultsUseCase } from '@core/racing/application/use-cases/SendFinalResultsUseCase'; -import { CloseRaceEventStewardingUseCase } from '@core/racing/application/use-cases/CloseRaceEventStewardingUseCase'; -import { InMemoryRaceEventRepository } from '@core/racing/infrastructure/repositories/InMemoryRaceEventRepository'; -import { InMemorySessionRepository } from '@core/racing/infrastructure/repositories/InMemorySessionRepository'; - -// Mock notification service -const mockNotificationService = { - sendNotification: vi.fn(), -}; - -// Test data builders -const createTestSession = (overrides: Partial<{ - id: string; - raceEventId: string; - sessionType: SessionType; - status: 'scheduled' | 'running' | 'completed'; - scheduledAt: Date; -}> = {}) => { - return Session.create({ - id: overrides.id ?? 'session-1', - raceEventId: overrides.raceEventId ?? 'race-event-1', - scheduledAt: overrides.scheduledAt ?? new Date(), - track: 'Monza', - car: 'F1 Car', - sessionType: overrides.sessionType ?? SessionType.main(), - status: overrides.status ?? 'scheduled', - }); -}; - -const createTestRaceEvent = (overrides: Partial<{ - id: string; - seasonId: string; - leagueId: string; - name: string; - sessions: Session[]; - status: 'scheduled' | 'in_progress' | 'awaiting_stewarding' | 'closed'; - stewardingClosesAt: Date; -}> = {}) => { - const sessions = overrides.sessions ?? [ - createTestSession({ id: 'practice-1', sessionType: SessionType.practice() }), - createTestSession({ id: 'qualifying-1', sessionType: SessionType.qualifying() }), - createTestSession({ id: 'main-1', sessionType: SessionType.main() }), - ]; - - return RaceEvent.create({ - id: overrides.id ?? 'race-event-1', - seasonId: overrides.seasonId ?? 'season-1', - leagueId: overrides.leagueId ?? 'league-1', - name: overrides.name ?? 'Monza Grand Prix', - sessions, - status: overrides.status ?? 'scheduled', - stewardingClosesAt: overrides.stewardingClosesAt, - }); -}; - -describe('Race Event Performance Summary Notifications', () => { - let raceEventRepository: InMemoryRaceEventRepository; - let sessionRepository: InMemorySessionRepository; - let sendPerformanceSummaryUseCase: SendPerformanceSummaryUseCase; - let sendFinalResultsUseCase: SendFinalResultsUseCase; - let closeStewardingUseCase: CloseRaceEventStewardingUseCase; - - beforeEach(() => { - raceEventRepository = new InMemoryRaceEventRepository(); - sessionRepository = new InMemorySessionRepository(); - sendPerformanceSummaryUseCase = new SendPerformanceSummaryUseCase( - mockNotificationService as any, - raceEventRepository as any, - {} as any // Mock result repository - ); - sendFinalResultsUseCase = new SendFinalResultsUseCase( - mockNotificationService as any, - raceEventRepository as any, - {} as any // Mock result repository - ); - closeStewardingUseCase = new CloseRaceEventStewardingUseCase( - raceEventRepository as any, - {} as any // Mock domain event publisher - ); - vi.clearAllMocks(); - }); - - describe('Performance Summary After Main Race Completion', () => { - it('should send performance summary notification when main race completes', async () => { - // Given - const raceEvent = createTestRaceEvent(); - await raceEventRepository.create(raceEvent); - - const mainRaceCompletedEvent = new MainRaceCompletedEvent({ - raceEventId: raceEvent.id, - sessionId: 'main-1', - leagueId: raceEvent.leagueId, - seasonId: raceEvent.seasonId, - completedAt: new Date(), - driverIds: ['driver-1', 'driver-2'], - }); - - // When - await sendPerformanceSummaryUseCase.execute(mainRaceCompletedEvent); - - // Then - expect(mockNotificationService.sendNotification).toHaveBeenCalledTimes(2); - expect(mockNotificationService.sendNotification).toHaveBeenCalledWith( - expect.objectContaining({ - recipientId: 'driver-1', - type: 'race_performance_summary', - urgency: 'modal', - title: expect.stringContaining('Race Complete'), - }) - ); - }); - - it('should calculate provisional rating changes correctly', async () => { - // Given - const raceEvent = createTestRaceEvent(); - await raceEventRepository.create(raceEvent); - - // Mock result repository to return position data - const mockResultRepository = { - findByRaceId: vi.fn().mockResolvedValue([ - { driverId: 'driver-1', position: 1, incidents: 0, getPositionChange: () => 0 }, - ]), - }; - - const useCase = new SendPerformanceSummaryUseCase( - mockNotificationService as any, - raceEventRepository as any, - mockResultRepository as any - ); - - const event = new MainRaceCompletedEvent({ - raceEventId: raceEvent.id, - sessionId: 'main-1', - leagueId: raceEvent.leagueId, - seasonId: raceEvent.seasonId, - completedAt: new Date(), - driverIds: ['driver-1'], - }); - - // When - await useCase.execute(event); - - // Then - expect(mockNotificationService.sendNotification).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ - provisionalRatingChange: 25, // P1 with 0 incidents = +25 - }), - }) - ); - }); - }); - - describe('Final Results After Stewarding Closes', () => { - it('should send final results notification when stewarding closes', async () => { - // Given - const raceEvent = createTestRaceEvent({ status: 'awaiting_stewarding' }); - await raceEventRepository.create(raceEvent); - - const stewardingClosedEvent = new RaceEventStewardingClosedEvent({ - raceEventId: raceEvent.id, - leagueId: raceEvent.leagueId, - seasonId: raceEvent.seasonId, - closedAt: new Date(), - driverIds: ['driver-1', 'driver-2'], - hadPenaltiesApplied: false, - }); - - // When - await sendFinalResultsUseCase.execute(stewardingClosedEvent); - - // Then - expect(mockNotificationService.sendNotification).toHaveBeenCalledTimes(2); - expect(mockNotificationService.sendNotification).toHaveBeenCalledWith( - expect.objectContaining({ - recipientId: 'driver-1', - type: 'race_final_results', - urgency: 'modal', - title: expect.stringContaining('Final Results'), - }) - ); - }); - }); - - describe('Stewarding Window Management', () => { - it('should close expired stewarding windows', async () => { - // Given - const pastDate = new Date(Date.now() - 25 * 60 * 60 * 1000); // 25 hours ago - const raceEvent = createTestRaceEvent({ - status: 'awaiting_stewarding', - stewardingClosesAt: pastDate, - }); - await raceEventRepository.create(raceEvent); - - // When - await closeStewardingUseCase.execute({}); - - // Then - const updatedEvent = await raceEventRepository.findById(raceEvent.id); - expect(updatedEvent?.status).toBe('closed'); - }); - - it('should not close unexpired stewarding windows', async () => { - // Given - const futureDate = new Date(Date.now() + 25 * 60 * 60 * 1000); // 25 hours from now - const raceEvent = createTestRaceEvent({ - status: 'awaiting_stewarding', - stewardingClosesAt: futureDate, - }); - await raceEventRepository.create(raceEvent); - - // When - await closeStewardingUseCase.execute({}); - - // Then - const updatedEvent = await raceEventRepository.findById(raceEvent.id); - expect(updatedEvent?.status).toBe('awaiting_stewarding'); - }); - }); - - describe('Race Event Lifecycle', () => { - it('should transition from scheduled to in_progress when sessions start', () => { - // Given - const raceEvent = createTestRaceEvent({ status: 'scheduled' }); - - // When - const startedEvent = raceEvent.start(); - - // Then - expect(startedEvent.status).toBe('in_progress'); - }); - - it('should transition to awaiting_stewarding when main race completes', () => { - // Given - const raceEvent = createTestRaceEvent({ status: 'in_progress' }); - - // When - const completedEvent = raceEvent.completeMainRace(); - - // Then - expect(completedEvent.status).toBe('awaiting_stewarding'); - }); - - it('should transition to closed when stewarding closes', () => { - // Given - const raceEvent = createTestRaceEvent({ status: 'awaiting_stewarding' }); - - // When - const closedEvent = raceEvent.closeStewarding(); - - // Then - expect(closedEvent.status).toBe('closed'); - }); - }); - - describe('Session Type Behavior', () => { - it('should identify main race sessions correctly', () => { - // Given - const mainSession = createTestSession({ sessionType: SessionType.main() }); - const practiceSession = createTestSession({ sessionType: SessionType.practice() }); - - // Then - expect(mainSession.countsForPoints()).toBe(true); - expect(practiceSession.countsForPoints()).toBe(false); - }); - - it('should identify qualifying sessions correctly', () => { - // Given - const qualiSession = createTestSession({ sessionType: SessionType.qualifying() }); - const mainSession = createTestSession({ sessionType: SessionType.main() }); - - // Then - expect(qualiSession.determinesGrid()).toBe(true); - expect(mainSession.determinesGrid()).toBe(false); - }); - }); -}); \ No newline at end of file diff --git a/tests/e2e/automation.e2e.test.ts b/tests/e2e/automation.e2e.test.ts deleted file mode 100644 index 5b8910f35..000000000 --- a/tests/e2e/automation.e2e.test.ts +++ /dev/null @@ -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, - ); - 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, - ); - 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, - ); - 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, - ); - 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, - ); - 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, - ); - 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, - ); -}); diff --git a/tests/e2e/companion/companion-ui-full-workflow.e2e.test.ts b/tests/e2e/companion/companion-ui-full-workflow.e2e.test.ts deleted file mode 100644 index 5a3cb7013..000000000 --- a/tests/e2e/companion/companion-ui-full-workflow.e2e.test.ts +++ /dev/null @@ -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 - ); -}); \ No newline at end of file diff --git a/tests/e2e/features/hosted-session-automation.feature b/tests/e2e/features/hosted-session-automation.feature deleted file mode 100644 index 66c6d5d21..000000000 --- a/tests/e2e/features/hosted-session-automation.feature +++ /dev/null @@ -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 \ No newline at end of file diff --git a/tests/e2e/hosted-real/cars-flow.real.e2e.test.ts b/tests/e2e/hosted-real/cars-flow.real.e2e.test.ts deleted file mode 100644 index 66e6e17e4..000000000 --- a/tests/e2e/hosted-real/cars-flow.real.e2e.test.ts +++ /dev/null @@ -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, - ); -}); \ No newline at end of file diff --git a/tests/e2e/hosted-real/login-and-wizard-smoke.e2e.test.ts b/tests/e2e/hosted-real/login-and-wizard-smoke.e2e.test.ts deleted file mode 100644 index 97c4d8cf9..000000000 --- a/tests/e2e/hosted-real/login-and-wizard-smoke.e2e.test.ts +++ /dev/null @@ -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, - ); -}); \ No newline at end of file diff --git a/tests/e2e/hosted-real/step-03-race-information.real.e2e.test.ts b/tests/e2e/hosted-real/step-03-race-information.real.e2e.test.ts deleted file mode 100644 index d743db020..000000000 --- a/tests/e2e/hosted-real/step-03-race-information.real.e2e.test.ts +++ /dev/null @@ -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, - ); -}); \ No newline at end of file diff --git a/tests/e2e/step-definitions/automation.steps.ts b/tests/e2e/step-definitions/automation.steps.ts deleted file mode 100644 index e07280e3c..000000000 --- a/tests/e2e/step-definitions/automation.steps.ts +++ /dev/null @@ -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 {}; \ No newline at end of file diff --git a/tests/e2e/steps/step-01-hosted-racing.e2e.test.ts b/tests/e2e/steps/step-01-hosted-racing.e2e.test.ts deleted file mode 100644 index 350436518..000000000 --- a/tests/e2e/steps/step-01-hosted-racing.e2e.test.ts +++ /dev/null @@ -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(); - }); -}); \ No newline at end of file diff --git a/tests/e2e/steps/step-02-create-race.e2e.test.ts b/tests/e2e/steps/step-02-create-race.e2e.test.ts deleted file mode 100644 index be137665a..000000000 --- a/tests/e2e/steps/step-02-create-race.e2e.test.ts +++ /dev/null @@ -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(); - }); -}); \ No newline at end of file diff --git a/tests/e2e/steps/step-03-race-information.e2e.test.ts b/tests/e2e/steps/step-03-race-information.e2e.test.ts deleted file mode 100644 index 0a12ae678..000000000 --- a/tests/e2e/steps/step-03-race-information.e2e.test.ts +++ /dev/null @@ -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); - }); -}); \ No newline at end of file diff --git a/tests/e2e/steps/step-04-server-details.e2e.test.ts b/tests/e2e/steps/step-04-server-details.e2e.test.ts deleted file mode 100644 index 51be5053f..000000000 --- a/tests/e2e/steps/step-04-server-details.e2e.test.ts +++ /dev/null @@ -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); - }); -}); \ No newline at end of file diff --git a/tests/e2e/steps/step-05-set-admins.e2e.test.ts b/tests/e2e/steps/step-05-set-admins.e2e.test.ts deleted file mode 100644 index 0ab348c84..000000000 --- a/tests/e2e/steps/step-05-set-admins.e2e.test.ts +++ /dev/null @@ -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'); - }); -}); \ No newline at end of file diff --git a/tests/e2e/steps/step-06-admins.e2e.test.ts b/tests/e2e/steps/step-06-admins.e2e.test.ts deleted file mode 100644 index a2de88517..000000000 --- a/tests/e2e/steps/step-06-admins.e2e.test.ts +++ /dev/null @@ -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'); - }); -}); \ No newline at end of file diff --git a/tests/e2e/steps/step-07-time-limits.e2e.test.ts b/tests/e2e/steps/step-07-time-limits.e2e.test.ts deleted file mode 100644 index f87e664dd..000000000 --- a/tests/e2e/steps/step-07-time-limits.e2e.test.ts +++ /dev/null @@ -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); - }); -}); \ No newline at end of file diff --git a/tests/e2e/steps/step-08-cars.e2e.test.ts b/tests/e2e/steps/step-08-cars.e2e.test.ts deleted file mode 100644 index 1f65c1c94..000000000 --- a/tests/e2e/steps/step-08-cars.e2e.test.ts +++ /dev/null @@ -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); - }); - }); -}); \ No newline at end of file diff --git a/tests/e2e/steps/step-09-add-car.e2e.test.ts b/tests/e2e/steps/step-09-add-car.e2e.test.ts deleted file mode 100644 index 3008079e3..000000000 --- a/tests/e2e/steps/step-09-add-car.e2e.test.ts +++ /dev/null @@ -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(); - }); - }); -}); \ No newline at end of file diff --git a/tests/e2e/steps/step-10-car-classes.e2e.test.ts b/tests/e2e/steps/step-10-car-classes.e2e.test.ts deleted file mode 100644 index b73ed208d..000000000 --- a/tests/e2e/steps/step-10-car-classes.e2e.test.ts +++ /dev/null @@ -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); - }); -}); \ No newline at end of file diff --git a/tests/e2e/steps/step-11-track.e2e.test.ts b/tests/e2e/steps/step-11-track.e2e.test.ts deleted file mode 100644 index 3c4990ac8..000000000 --- a/tests/e2e/steps/step-11-track.e2e.test.ts +++ /dev/null @@ -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); - }); - }); -}); \ No newline at end of file diff --git a/tests/e2e/steps/step-12-add-track.e2e.test.ts b/tests/e2e/steps/step-12-add-track.e2e.test.ts deleted file mode 100644 index 8a174834f..000000000 --- a/tests/e2e/steps/step-12-add-track.e2e.test.ts +++ /dev/null @@ -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); - }); -}); \ No newline at end of file diff --git a/tests/e2e/steps/step-13-track-options.e2e.test.ts b/tests/e2e/steps/step-13-track-options.e2e.test.ts deleted file mode 100644 index 05ba3f99f..000000000 --- a/tests/e2e/steps/step-13-track-options.e2e.test.ts +++ /dev/null @@ -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(); - }); -}); \ No newline at end of file diff --git a/tests/e2e/steps/step-14-time-of-day.e2e.test.ts b/tests/e2e/steps/step-14-time-of-day.e2e.test.ts deleted file mode 100644 index ea1ae94ad..000000000 --- a/tests/e2e/steps/step-14-time-of-day.e2e.test.ts +++ /dev/null @@ -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); - }); -}); \ No newline at end of file diff --git a/tests/e2e/steps/step-15-weather.e2e.test.ts b/tests/e2e/steps/step-15-weather.e2e.test.ts deleted file mode 100644 index daa608b55..000000000 --- a/tests/e2e/steps/step-15-weather.e2e.test.ts +++ /dev/null @@ -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)); - } - }); -}); \ No newline at end of file diff --git a/tests/e2e/steps/step-16-race-options.e2e.test.ts b/tests/e2e/steps/step-16-race-options.e2e.test.ts deleted file mode 100644 index 3ba8f78dc..000000000 --- a/tests/e2e/steps/step-16-race-options.e2e.test.ts +++ /dev/null @@ -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); - }); -}); \ No newline at end of file diff --git a/tests/e2e/steps/step-17-team-driving.e2e.test.ts b/tests/e2e/steps/step-17-team-driving.e2e.test.ts deleted file mode 100644 index d8b4a0eee..000000000 --- a/tests/e2e/steps/step-17-team-driving.e2e.test.ts +++ /dev/null @@ -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); - }); -}); \ No newline at end of file diff --git a/tests/e2e/steps/step-18-track-conditions.e2e.test.ts b/tests/e2e/steps/step-18-track-conditions.e2e.test.ts deleted file mode 100644 index 02847cac6..000000000 --- a/tests/e2e/steps/step-18-track-conditions.e2e.test.ts +++ /dev/null @@ -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); - }); -}); \ No newline at end of file diff --git a/tests/e2e/support/AutoNavGuard.ts b/tests/e2e/support/AutoNavGuard.ts deleted file mode 100644 index 1e921012c..000000000 --- a/tests/e2e/support/AutoNavGuard.ts +++ /dev/null @@ -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): 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, -): Promise { - assertAutoNavigationConfig(config); - return adapter.executeStep(StepId.create(step), config); -} \ No newline at end of file diff --git a/tests/e2e/support/PermissionGuard.ts b/tests/e2e/support/PermissionGuard.ts deleted file mode 100644 index 3b6b39ed2..000000000 --- a/tests/e2e/support/PermissionGuard.ts +++ /dev/null @@ -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 { - 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 { - 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 { - // 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 { - 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 { - 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 { - 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 { - 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}` - ); - } -} \ No newline at end of file diff --git a/tests/e2e/support/StepHarness.ts b/tests/e2e/support/StepHarness.ts deleted file mode 100644 index 229fb63f6..000000000 --- a/tests/e2e/support/StepHarness.ts +++ /dev/null @@ -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; - executeStep(step: number, config: Record): Promise; - executeStepWithAutoNavigation( - step: number, - config: Record, - ): Promise; - executeStepWithFixtureMismatch( - step: number, - config: Record, - ): Promise; - dispose(): Promise; -} - -async function createRealAdapter(baseUrl: string): Promise { - 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 { - 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 { - const server = new FixtureServer(); - const { url } = await server.start(); - - const adapter = useMock ? await createMockAdapter() : await createRealAdapter(url); - - async function navigateToFixtureStep(step: number): Promise { - await adapter.navigateToPage(server.getFixtureUrl(step)); - await adapter.getPage()?.waitForLoadState('domcontentloaded'); - } - - async function executeStepWithAutoNavigation( - step: number, - config: Record, - ): Promise { - 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, - ): Promise { - return adapter.executeStep(StepId.create(step), { - ...config, - __skipFixtureNavigation: true, - }); - } - - async function executeStep( - step: number, - config: Record, - ): Promise { - return executeStepWithFixtureMismatch(step, config); - } - - async function dispose(): Promise { - await adapter.disconnect(); - await server.stop(); - } - - return { - server, - adapter, - baseUrl: url, - getFixtureUrl: (step) => server.getFixtureUrl(step), - navigateToFixtureStep, - executeStep, - executeStepWithAutoNavigation, - executeStepWithFixtureMismatch, - dispose, - }; -} \ No newline at end of file diff --git a/tests/e2e/validators/hosted-validator-guards.e2e.test.ts b/tests/e2e/validators/hosted-validator-guards.e2e.test.ts deleted file mode 100644 index ec85286d5..000000000 --- a/tests/e2e/validators/hosted-validator-guards.e2e.test.ts +++ /dev/null @@ -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, - ); -}); \ No newline at end of file diff --git a/tests/e2e/workflows/full-hosted-session.autonav.workflow.e2e.test.ts b/tests/e2e/workflows/full-hosted-session.autonav.workflow.e2e.test.ts deleted file mode 100644 index 63bedd527..000000000 --- a/tests/e2e/workflows/full-hosted-session.autonav.workflow.e2e.test.ts +++ /dev/null @@ -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, - ); -}); \ No newline at end of file diff --git a/tests/e2e/workflows/full-hosted-session.workflow.e2e.test.ts b/tests/e2e/workflows/full-hosted-session.workflow.e2e.test.ts deleted file mode 100644 index b8e527f76..000000000 --- a/tests/e2e/workflows/full-hosted-session.workflow.e2e.test.ts +++ /dev/null @@ -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(); - }, - ); -}); \ No newline at end of file diff --git a/tests/e2e/workflows/steps-07-09-cars-flow.e2e.test.ts b/tests/e2e/workflows/steps-07-09-cars-flow.e2e.test.ts deleted file mode 100644 index ff6804b13..000000000 --- a/tests/e2e/workflows/steps-07-09-cars-flow.e2e.test.ts +++ /dev/null @@ -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); - }, - ); -}); \ No newline at end of file diff --git a/tests/integration/automation/OverlaySyncService.test.ts b/tests/integration/automation/OverlaySyncService.test.ts deleted file mode 100644 index abbd7dade..000000000 --- a/tests/integration/automation/OverlaySyncService.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import "reflect-metadata"; -import { container } from "tsyringe"; -import { configureDIContainer, resetDIContainer } from "../../../apps/companion/main/di-config"; -import { DI_TOKENS } from "../../../apps/companion/main/di-tokens"; -import { OverlaySyncService } from "@gridpilot/automation/application/services/OverlaySyncService"; -import { LoggerPort } from "@gridpilot/automation/application/ports/LoggerPort"; -import { IAutomationLifecycleEmitter, LifecycleCallback } from "@gridpilot/automation/infrastructure//IAutomationLifecycleEmitter"; -import { AutomationEventPublisherPort, AutomationEvent } from "@gridpilot/automation/application/ports/AutomationEventPublisherPort"; -import { ConsoleLogAdapter } from "@gridpilot/automation/infrastructure//logging/ConsoleLogAdapter"; -import { describe, it, expect, beforeEach, afterEach, vi, SpyInstance } from 'vitest'; - -describe("OverlaySyncService Integration with ConsoleLogAdapter", () => { - let consoleErrorSpy: SpyInstance<[message?: any, ...optionalParams: any[]], void>; - let consoleWarnSpy: SpyInstance<[message?: any, ...optionalParams: any[]], void>; - let originalNodeEnv: string | undefined; - - beforeEach(() => { - originalNodeEnv = process.env.NODE_ENV; - process.env.NODE_ENV = 'development'; - - resetDIContainer(); - configureDIContainer(); - - consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); - }); - - afterEach(() => { - consoleErrorSpy.mockRestore(); - consoleWarnSpy.mockRestore(); - if (originalNodeEnv !== undefined) { - process.env.NODE_ENV = originalNodeEnv; - } - resetDIContainer(); - }); - - it("should use ConsoleLogAdapter and log messages when OverlaySyncService encounters an error", async () => { - const logger = container.resolve(DI_TOKENS.Logger); - const overlaySyncService = container.resolve(DI_TOKENS.OverlaySyncPort); - - expect(logger).toBeInstanceOf(ConsoleLogAdapter); - - const mockLifecycleEmitter: IAutomationLifecycleEmitter = { - onLifecycle: vi.fn((_cb: LifecycleCallback) => { - throw new Error("Test lifecycle emitter error"); - }), - offLifecycle: vi.fn(), - }; - - const mockPublisher: AutomationEventPublisherPort = { - publish: vi.fn(), - }; - - const serviceWithMockedEmitter = new OverlaySyncService({ - lifecycleEmitter: mockLifecycleEmitter, - publisher: mockPublisher, - logger: logger, - }); - - const action = { id: "test-action-1", label: "Test Action" }; - await serviceWithMockedEmitter.execute(action); - - expect(consoleErrorSpy).toHaveBeenCalledWith( - expect.stringContaining("OverlaySyncService: failed to subscribe to lifecycleEmitter"), - expect.any(Error), - expect.objectContaining({ actionId: action.id }), - ); - expect(consoleWarnSpy).not.toHaveBeenCalled(); - }); - - it("should use ConsoleLogAdapter and log warn messages when OverlaySyncService fails to publish", async () => { - const logger = container.resolve(DI_TOKENS.Logger); - expect(logger).toBeInstanceOf(ConsoleLogAdapter); - - const mockLifecycleEmitter: IAutomationLifecycleEmitter = { - onLifecycle: vi.fn(), - offLifecycle: vi.fn(), - }; - - const mockPublisher: AutomationEventPublisherPort = { - publish: vi.fn((_event: AutomationEvent) => { - throw new Error("Test publish error"); - }), - }; - - const serviceWithMockedPublisher = new OverlaySyncService({ - lifecycleEmitter: mockLifecycleEmitter, - publisher: mockPublisher, - logger: logger, - }); - - const action = { id: "test-action-2", label: "Test Action" }; - await serviceWithMockedPublisher.execute(action); - - expect(consoleWarnSpy).toHaveBeenCalledWith( - expect.stringContaining("OverlaySyncService: publisher.publish failed"), - expect.objectContaining({ - actionId: action.id, - error: expect.any(Error), - }), - ); - expect(consoleErrorSpy).not.toHaveBeenCalled(); - }); -}); diff --git a/tests/integration/infrastructure/BrowserModeIntegration.test.ts b/tests/integration/infrastructure/BrowserModeIntegration.test.ts deleted file mode 100644 index 0c0a5ca62..000000000 --- a/tests/integration/infrastructure/BrowserModeIntegration.test.ts +++ /dev/null @@ -1,361 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from 'vitest'; -import * as fs from 'fs'; -import * as path from 'path'; -import type { LoggerPort } from 'apps/companion/main/automation/application/ports/LoggerPort'; -import type { LogContext } from 'apps/companion/main/automation/application/ports/LoggerContext'; - -/** - * Integration tests for Browser Mode in PlaywrightAutomationAdapter - GREEN PHASE - * - * These tests verify that the adapter correctly applies headed/headless mode based on NODE_ENV - * and runtime configuration via BrowserModeConfigLoader. - */ - -type BrowserModeSource = 'env' | 'file' | 'default'; - -interface PlaywrightAutomationAdapterLike { - connect(): Promise<{ success: boolean; error?: string }>; - disconnect(): Promise; - isConnected(): boolean; - getBrowserMode(): 'headed' | 'headless'; - getBrowserModeSource(): BrowserModeSource; -} - -describe('Browser Mode Integration - GREEN Phase', () => { - const originalEnv = process.env; - let adapter: PlaywrightAutomationAdapterLike | null = null; - - let unhandledRejectionHandler: ((reason: unknown) => void) | null = null; - - beforeEach(() => { - process.env = { ...originalEnv }; - Object.defineProperty(process.env, 'NODE_ENV', { - value: undefined, - writable: true, - enumerable: true, - configurable: true - }); - }); - - beforeAll(() => { - unhandledRejectionHandler = (reason: unknown) => { - const message = - reason instanceof Error ? reason.message : String(reason ?? ''); - if (message.includes('cdpSession.send: Target page, context or browser has been closed')) { - return; - } - throw reason; - }; - process.on('unhandledRejection', unhandledRejectionHandler); - }); - - afterEach(async () => { - if (adapter) { - await adapter.disconnect(); - adapter = null; - } - - process.env = originalEnv; - }); - - afterAll(() => { - if (unhandledRejectionHandler) { - (process as any).removeListener('unhandledRejection', unhandledRejectionHandler); - unhandledRejectionHandler = null; - } - }); - - describe('Headed Mode Launch (NODE_ENV=development, default)', () => { - it('should launch browser with headless: false when NODE_ENV=development by default', async () => { - // Skip: Tests must always run headless to avoid opening browsers - // This test validated behavior for development mode which is not applicable in test environment - }); - - it('should show browser window in development mode by default', async () => { - // Skip: Tests must always run headless to avoid opening browsers - // This test validated behavior for development mode which is not applicable in test environment - }); - }); - - describe('Headless Mode Launch (NODE_ENV=production/test)', () => { - it('should launch browser with headless: true when NODE_ENV=production', async () => { - Object.defineProperty(process.env, 'NODE_ENV', { - value: 'production', - writable: true, - enumerable: true, - configurable: true - }); - - const { PlaywrightAutomationAdapter } = await import( - 'core/automation/infrastructure//automation' - ); - - adapter = new PlaywrightAutomationAdapter({ - mode: 'mock', - }, undefined, undefined); - - const result = await adapter.connect(); - - expect(result.success).toBe(true); - expect(adapter.getBrowserMode()).toBe('headless'); - expect(adapter.getBrowserModeSource()).toBe('NODE_ENV'); - expect(adapter.isConnected()).toBe(true); - }); - - it('should launch browser with headless: true when NODE_ENV=test', async () => { - Object.defineProperty(process.env, 'NODE_ENV', { - value: 'test', - writable: true, - enumerable: true, - configurable: true - }); - - const { PlaywrightAutomationAdapter } = await import( - 'core/automation/infrastructure//automation' - ); - - adapter = new PlaywrightAutomationAdapter({ - mode: 'mock', - }, undefined, undefined); - - const result = await adapter.connect(); - - expect(result.success).toBe(true); - expect(adapter.getBrowserMode()).toBe('headless'); - expect(adapter.getBrowserModeSource()).toBe('NODE_ENV'); - expect(adapter.isConnected()).toBe(true); - }); - - it('should default to headless when NODE_ENV is not set', async () => { - Object.defineProperty(process.env, 'NODE_ENV', { - value: undefined, - writable: true, - enumerable: true, - configurable: true - }); - - const { PlaywrightAutomationAdapter } = await import( - 'core/automation/infrastructure//automation' - ); - - adapter = new PlaywrightAutomationAdapter({ - mode: 'mock', - }, undefined, undefined); - - await adapter.connect(); - - expect(adapter.getBrowserMode()).toBe('headless'); - expect(adapter.getBrowserModeSource()).toBe('NODE_ENV'); - }); - }); - - describe('Source Tracking', () => { - it('should report GUI as source in development mode', async () => { - // Skip: Tests must always run headless to avoid opening browsers - // This test validated behavior for development mode which is not applicable in test environment - }); - - it('should report NODE_ENV as source in production mode', async () => { - Object.defineProperty(process.env, 'NODE_ENV', { - value: 'production', - writable: true, - enumerable: true, - configurable: true - }); - - const { PlaywrightAutomationAdapter } = await import( - 'core/automation/infrastructure//automation' - ); - - adapter = new PlaywrightAutomationAdapter({ - mode: 'mock', - }, undefined, undefined); - - await adapter.connect(); - - expect(adapter.getBrowserModeSource()).toBe('NODE_ENV'); - }); - - it('should report NODE_ENV as source in test mode', async () => { - Object.defineProperty(process.env, 'NODE_ENV', { - value: 'test', - writable: true, - enumerable: true, - configurable: true - }); - - const { PlaywrightAutomationAdapter } = await import( - 'core/automation/infrastructure//automation' - ); - - adapter = new PlaywrightAutomationAdapter({ - mode: 'mock', - }, undefined); - - await adapter.connect(); - - expect(adapter.getBrowserModeSource()).toBe('NODE_ENV'); - }); - }); - - describe('Logging', () => { - it('should log browser mode configuration with GUI source in development', async () => { - // Skip: Tests must always run headless to avoid opening browsers - // This test validated behavior for development mode which is not applicable in test environment - }); - - it('should log browser mode configuration with NODE_ENV source in production', async () => { - (process.env as any).NODE_ENV = 'production'; - - const logSpy: Array<{ level: string; message: string; context?: Record }> = []; - type LoggerLike = { - debug: (message: string, context?: Record) => void; - info: (message: string, context?: Record) => void; - warn: (message: string, context?: Record) => void; - error: (message: string, error?: Error, context?: Record) => void; - fatal: (message: string, error?: Error, context?: Record) => void; - child: (context: Record) => LoggerLike; - flush: () => Promise; - }; - const mockLogger: LoggerLike = { - debug: (message: string, context?: Record) => logSpy.push({ level: 'debug', message, ...(context ? { context } : {}) }), - info: (message: string, context?: Record) => logSpy.push({ level: 'info', message, ...(context ? { context } : {}) }), - warn: (message: string, context?: Record) => logSpy.push({ level: 'warn', message, ...(context ? { context } : {}) }), - error: (message: string, error?: Error, context?: Record) => logSpy.push({ level: 'error', message, ...(context ? { context } : {}) }), - fatal: (message: string, error?: Error, context?: Record) => logSpy.push({ level: 'fatal', message, ...(context ? { context } : {}) }), - child: (context: Record) => mockLogger, - flush: () => Promise.resolve(), - }; - - const { PlaywrightAutomationAdapter } = await import( - 'core/automation/infrastructure//automation' - ); - - adapter = new PlaywrightAutomationAdapter( - { mode: 'mock' }, - mockLogger - ); - - await adapter.connect(); - - // Should have logged browser mode config - const browserModeLog = logSpy.find( - (log) => log.message.includes('browser mode') || log.message.includes('Browser mode') - ); - - expect(browserModeLog).toBeDefined(); - expect(browserModeLog?.context?.mode).toBe('headless'); - expect(browserModeLog?.context?.source).toBe('NODE_ENV'); - }); - }); - - describe('Persistent Context', () => { - it('should apply browser mode to persistent browser context', async () => { - Object.defineProperty(process.env, 'NODE_ENV', { - value: 'production', - writable: true, - enumerable: true, - configurable: true - }); - - const { PlaywrightAutomationAdapter } = await import( - 'core/automation/infrastructure//automation' - ); - - const userDataDir = path.join(process.cwd(), 'test-browser-data'); - - adapter = new PlaywrightAutomationAdapter({ - mode: 'real', - userDataDir, - }, undefined, undefined); - - await adapter.connect(); - - expect(adapter.getBrowserMode()).toBe('headless'); - - // Cleanup - await adapter.disconnect(); - if (fs.existsSync(userDataDir)) { - fs.rmSync(userDataDir, { recursive: true, force: true }); - } - }); - }); - - describe('Runtime loader re-read instrumentation (test-only)', () => { - it('reads mode from injected loader and passes headless flag to launcher accordingly', async () => { - Object.defineProperty(process.env, 'NODE_ENV', { - value: 'development', - writable: true, - enumerable: true, - configurable: true - }); - const { PlaywrightAutomationAdapter } = await import( - 'core/automation/infrastructure//automation' - ); - const { BrowserModeConfigLoader } = await import( - '../../../apps/companion/main/automation/infrastructure/config/BrowserModeConfig' - ); - - // Create loader and set to headed - const loader = new BrowserModeConfigLoader(); - loader.setDevelopmentMode('headed'); - - // Capture launch options - type LaunchOptions = { headless?: boolean; [key: string]: unknown }; - const launches: Array<{ type: string; opts?: LaunchOptions; userDataDir?: string }> = []; - - const mockLauncher = { - launch: async (opts: LaunchOptions) => { - launches.push({ type: 'launch', opts }); - return { - newContext: async () => ({ - newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }), - close: async () => {}, - }), - newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }), - close: async () => {}, - newContextSync: () => {}, - }; - }, - launchPersistentContext: async (userDataDir: string, opts: LaunchOptions) => { - launches.push({ type: 'launchPersistent', userDataDir, opts }); - return { - pages: () => [{ setDefaultTimeout: () => {}, close: async () => {} }], - newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }), - close: async () => {}, - }; - }, - }; - - // Inject test launcher - const AdapterWithTestLauncher = PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & { - testLauncher?: typeof mockLauncher; - }; - AdapterWithTestLauncher.testLauncher = mockLauncher; - - adapter = new PlaywrightAutomationAdapter({ mode: 'mock' }, undefined, loader); - - // First connect => loader says headed => headless should be false - const r1 = await adapter.connect(); - expect(r1.success).toBe(true); - expect(launches.length).toBeGreaterThan(0); - expect((launches[0] as any).opts.headless).toBe(false); - - // Disconnect and change loader to headless - await adapter.disconnect(); - loader.setDevelopmentMode('headless'); - - // Second connect => headless true - const r2 = await adapter.connect(); - expect(r2.success).toBe(true); - // The second recorded launch may be at index 1 if both calls used the same launcher path - const secondLaunch = launches.slice(1).find(l => l.type === 'launch' || l.type === 'launchPersistent'); - expect(secondLaunch).toBeDefined(); - expect(secondLaunch!.opts?.headless).toBe(true); - - // Cleanup test hook - (AdapterWithTestLauncher as any).testLauncher = undefined; - await adapter.disconnect(); - }); - }); -}); \ No newline at end of file diff --git a/tests/integration/infrastructure/CheckoutPriceExtractor.test.ts b/tests/integration/infrastructure/CheckoutPriceExtractor.test.ts deleted file mode 100644 index f2586d22b..000000000 --- a/tests/integration/infrastructure/CheckoutPriceExtractor.test.ts +++ /dev/null @@ -1,378 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { Result } from '@gridpilot/shared/application/Result'; -import { CheckoutPriceExtractor } from '../../../apps/companion/main/automation/infrastructure/automation/CheckoutPriceExtractor'; -import { CheckoutStateEnum } from 'apps/companion/main/automation/domain/value-objects/CheckoutState'; - -/** - * CheckoutPriceExtractor Integration Tests - GREEN PHASE - * - * Tests verify HTML parsing for checkout price extraction and state detection. - */ - -type Page = ConstructorParameters[0]; -type Locator = ReturnType; - -describe('CheckoutPriceExtractor Integration', () => { - let mockPage: Page; - let mockLocator: any; - let mockPillLocator: any; - - beforeEach(() => { - // Create nested locator mock for span.label-pill - mockPillLocator = { - textContent: vi.fn().mockResolvedValue('$0.50'), - first: vi.fn().mockReturnThis(), - locator: vi.fn().mockReturnThis(), - }; - - mockLocator = { - getAttribute: vi.fn(), - innerHTML: vi.fn(), - textContent: vi.fn(), - locator: vi.fn(() => mockPillLocator), - first: vi.fn().mockReturnThis(), - }; - - mockPage = { - locator: vi.fn((selector) => { - if (selector === '.label-pill, .label-inverse') { - return mockPillLocator; - } - return mockLocator; - }), - }; - }); - - describe('Success state HTML extraction', () => { - it('should extract $0.50 from success button', async () => { - const buttonHtml = '$0.50'; - - mockLocator.getAttribute.mockResolvedValue('btn btn-success'); - mockLocator.innerHTML.mockResolvedValue(buttonHtml); - mockPillLocator.textContent.mockResolvedValue('$0.50'); - - const extractor = new CheckoutPriceExtractor(mockPage); - const result = await extractor.extractCheckoutInfo(); - - expect(result.isOk()).toBe(true); - const info = result.unwrap(); - expect(info.price).not.toBeNull(); - expect(info.price!.getAmount()).toBe(0.50); - expect(info.state.getValue()).toBe(CheckoutStateEnum.READY); - }); - - it('should extract $5.00 from success button', async () => { - const buttonHtml = '$5.00'; - - mockLocator.getAttribute.mockResolvedValue('btn btn-success'); - mockLocator.innerHTML.mockResolvedValue(buttonHtml); - mockPillLocator.textContent.mockResolvedValue('$5.00'); - - const extractor = new CheckoutPriceExtractor(mockPage); - const result = await extractor.extractCheckoutInfo(); - - expect(result.isOk()).toBe(true); - const info = result.unwrap(); - expect(info.price!.getAmount()).toBe(5.00); - expect(info.state.getValue()).toBe(CheckoutStateEnum.READY); - }); - - it('should extract $100.00 from success button', async () => { - const buttonHtml = '$100.00'; - - mockLocator.getAttribute.mockResolvedValue('btn btn-success'); - mockLocator.innerHTML.mockResolvedValue(buttonHtml); - mockPillLocator.textContent.mockResolvedValue('$100.00'); - - const extractor = new CheckoutPriceExtractor(mockPage); - const result = await extractor.extractCheckoutInfo(); - - expect(result.isOk()).toBe(true); - const info = result.unwrap(); - expect(info.price!.getAmount()).toBe(100.00); - expect(info.state.getValue()).toBe(CheckoutStateEnum.READY); - }); - - it('should detect READY state from btn-success class', async () => { - const buttonHtml = '$0.50'; - - mockLocator.getAttribute.mockResolvedValue('btn btn-success'); - mockLocator.innerHTML.mockResolvedValue(buttonHtml); - mockPillLocator.textContent.mockResolvedValue('$0.50'); - - const extractor = new CheckoutPriceExtractor(mockPage); - const result = await extractor.extractCheckoutInfo(); - - expect(result.isOk()).toBe(true); - expect(result.unwrap().state.getValue()).toBe(CheckoutStateEnum.READY); - }); - }); - - describe('Insufficient funds HTML detection', () => { - it('should detect INSUFFICIENT_FUNDS when btn-success is missing', async () => { - const buttonHtml = '$0.50'; - - mockLocator.getAttribute.mockResolvedValue('btn btn-default'); - mockLocator.innerHTML.mockResolvedValue(buttonHtml); - mockPillLocator.textContent.mockResolvedValue('$0.50'); - - const extractor = new CheckoutPriceExtractor(mockPage); - const result = await extractor.extractCheckoutInfo(); - - expect(result.isOk()).toBe(true); - const info = result.unwrap(); - expect(info.price).not.toBeNull(); - expect(info.price!.getAmount()).toBe(0.50); - expect(info.state.getValue()).toBe(CheckoutStateEnum.INSUFFICIENT_FUNDS); - }); - - it('should still extract price when funds are insufficient', async () => { - const buttonHtml = '$10.00'; - - mockLocator.getAttribute.mockResolvedValue('btn btn-default'); - mockLocator.innerHTML.mockResolvedValue(buttonHtml); - mockPillLocator.textContent.mockResolvedValue('$10.00'); - - const extractor = new CheckoutPriceExtractor(mockPage); - const result = await extractor.extractCheckoutInfo(); - - expect(result.isOk()).toBe(true); - const info = result.unwrap(); - expect(info.price!.getAmount()).toBe(10.00); - expect(info.state.getValue()).toBe(CheckoutStateEnum.INSUFFICIENT_FUNDS); - }); - - it('should detect btn-primary as insufficient funds', async () => { - const buttonHtml = '$0.50'; - - mockLocator.getAttribute.mockResolvedValue('btn btn-primary'); - mockLocator.innerHTML.mockResolvedValue(buttonHtml); - mockPillLocator.textContent.mockResolvedValue('$0.50'); - - const extractor = new CheckoutPriceExtractor(mockPage); - const result = await extractor.extractCheckoutInfo(); - - expect(result.isOk()).toBe(true); - expect(result.unwrap().state.getValue()).toBe(CheckoutStateEnum.INSUFFICIENT_FUNDS); - }); - }); - - describe('Price parsing variations', () => { - it('should parse price with nested span tags', async () => { - const buttonHtml = '$0.50'; - - mockLocator.getAttribute.mockResolvedValue('btn btn-success'); - mockLocator.innerHTML.mockResolvedValue(buttonHtml); - mockPillLocator.textContent.mockResolvedValue('$0.50'); - - const extractor = new CheckoutPriceExtractor(mockPage); - const result = await extractor.extractCheckoutInfo(); - - expect(result.isOk()).toBe(true); - expect(result.unwrap().price!.getAmount()).toBe(0.50); - }); - - it('should parse price with whitespace', async () => { - const buttonHtml = ' $0.50 '; - - mockLocator.getAttribute.mockResolvedValue('btn btn-success'); - mockLocator.innerHTML.mockResolvedValue(buttonHtml); - mockPillLocator.textContent.mockResolvedValue(' $0.50 '); - - const extractor = new CheckoutPriceExtractor(mockPage); - const result = await extractor.extractCheckoutInfo(); - - expect(result.isOk()).toBe(true); - expect(result.unwrap().price!.getAmount()).toBe(0.50); - }); - - it('should parse price with multiple classes', async () => { - const buttonHtml = '$0.50'; - - mockLocator.getAttribute.mockResolvedValue('btn btn-lg btn-success pull-right'); - mockLocator.innerHTML.mockResolvedValue(buttonHtml); - mockPillLocator.textContent.mockResolvedValue('$0.50'); - - const extractor = new CheckoutPriceExtractor(mockPage); - const result = await extractor.extractCheckoutInfo(); - - expect(result.isOk()).toBe(true); - expect(result.unwrap().price!.getAmount()).toBe(0.50); - expect(result.unwrap().state.getValue()).toBe(CheckoutStateEnum.READY); - }); - }); - - describe('Missing button handling', () => { - it('should return UNKNOWN state when button not found', async () => { - mockLocator.getAttribute.mockResolvedValue(null); - mockLocator.innerHTML.mockRejectedValue(new Error('Element not found')); - - const extractor = new CheckoutPriceExtractor(mockPage); - const result = await extractor.extractCheckoutInfo(); - - expect(result.isOk()).toBe(true); - const info = result.unwrap(); - expect(info.price).toBeNull(); - expect(info.state.getValue()).toBe(CheckoutStateEnum.UNKNOWN); - }); - - it('should return null price when button not found', async () => { - mockLocator.getAttribute.mockResolvedValue(null); - mockPillLocator.textContent.mockResolvedValue(null); - - const extractor = new CheckoutPriceExtractor(mockPage); - const result = await extractor.extractCheckoutInfo(); - - expect(result.isOk()).toBe(true); - expect(result.unwrap().price).toBeNull(); - }); - }); - - describe('Malformed HTML handling', () => { - it('should return null price when price text is invalid', async () => { - const buttonHtml = 'Invalid Price'; - - mockLocator.getAttribute.mockResolvedValue('btn btn-success'); - mockLocator.innerHTML.mockResolvedValue(buttonHtml); - mockPillLocator.textContent.mockResolvedValue('Invalid Price'); - - const extractor = new CheckoutPriceExtractor(mockPage); - const result = await extractor.extractCheckoutInfo(); - - expect(result.isOk()).toBe(true); - const info = result.unwrap(); - expect(info.price).toBeNull(); - expect(info.state.getValue()).toBe(CheckoutStateEnum.READY); - }); - - it('should return null price when price is missing dollar sign', async () => { - const buttonHtml = '0.50'; - - mockLocator.getAttribute.mockResolvedValue('btn btn-success'); - mockLocator.innerHTML.mockResolvedValue(buttonHtml); - mockPillLocator.textContent.mockResolvedValue('0.50'); - - const extractor = new CheckoutPriceExtractor(mockPage); - const result = await extractor.extractCheckoutInfo(); - - expect(result.isOk()).toBe(true); - expect(result.unwrap().price).toBeNull(); - }); - - it('should handle empty price text', async () => { - const buttonHtml = ''; - - mockLocator.getAttribute.mockResolvedValue('btn btn-success'); - mockLocator.innerHTML.mockResolvedValue(buttonHtml); - mockPillLocator.textContent.mockResolvedValue(''); - - const extractor = new CheckoutPriceExtractor(mockPage); - const result = await extractor.extractCheckoutInfo(); - - expect(result.isOk()).toBe(true); - expect(result.unwrap().price).toBeNull(); - }); - }); - - describe('Button HTML capture', () => { - it('should capture full button HTML for debugging', async () => { - const buttonHtml = '$0.50'; - - mockLocator.getAttribute.mockResolvedValue('btn btn-success'); - mockLocator.innerHTML.mockResolvedValue(buttonHtml); - mockPillLocator.textContent.mockResolvedValue('$0.50'); - - const extractor = new CheckoutPriceExtractor(mockPage); - const result = await extractor.extractCheckoutInfo(); - - expect(result.isOk()).toBe(true); - expect(result.unwrap().buttonHtml).toBe(buttonHtml); - }); - - it('should capture button HTML even when price parsing fails', async () => { - const buttonHtml = 'Invalid'; - - mockLocator.getAttribute.mockResolvedValue('btn btn-success'); - mockLocator.innerHTML.mockResolvedValue(buttonHtml); - mockPillLocator.textContent.mockResolvedValue('Invalid'); - - const extractor = new CheckoutPriceExtractor(mockPage); - const result = await extractor.extractCheckoutInfo(); - - expect(result.isOk()).toBe(true); - expect(result.unwrap().buttonHtml).toBe(buttonHtml); - }); - - it('should return empty buttonHtml when button not found', async () => { - mockLocator.getAttribute.mockResolvedValue(null); - mockLocator.innerHTML.mockResolvedValue(''); - - const extractor = new CheckoutPriceExtractor(mockPage); - const result = await extractor.extractCheckoutInfo(); - - expect(result.isOk()).toBe(true); - expect(result.unwrap().buttonHtml).toBe(''); - }); - }); - - describe('BDD Scenarios', () => { - it('Given checkout button with $0.50 and btn-success, When extracting, Then price is $0.50 and state is READY', async () => { - const buttonHtml = '$0.50'; - - mockLocator.getAttribute.mockResolvedValue('btn btn-success'); - mockLocator.innerHTML.mockResolvedValue(buttonHtml); - mockPillLocator.textContent.mockResolvedValue('$0.50'); - - const extractor = new CheckoutPriceExtractor(mockPage); - const result = await extractor.extractCheckoutInfo(); - - expect(result.isOk()).toBe(true); - const info = result.unwrap(); - expect(info.price!.getAmount()).toBe(0.50); - expect(info.state.getValue()).toBe(CheckoutStateEnum.READY); - }); - - it('Given checkout button with $0.50 without btn-success, When extracting, Then state is INSUFFICIENT_FUNDS', async () => { - const buttonHtml = '$0.50'; - - mockLocator.getAttribute.mockResolvedValue('btn btn-default'); - mockLocator.innerHTML.mockResolvedValue(buttonHtml); - mockPillLocator.textContent.mockResolvedValue('$0.50'); - - const extractor = new CheckoutPriceExtractor(mockPage); - const result = await extractor.extractCheckoutInfo(); - - expect(result.isOk()).toBe(true); - expect(result.unwrap().state.getValue()).toBe(CheckoutStateEnum.INSUFFICIENT_FUNDS); - }); - - it('Given button not found, When extracting, Then state is UNKNOWN and price is null', async () => { - mockLocator.getAttribute.mockResolvedValue(null); - mockLocator.innerHTML.mockResolvedValue(''); - - const extractor = new CheckoutPriceExtractor(mockPage); - const result = await extractor.extractCheckoutInfo(); - - expect(result.isOk()).toBe(true); - const info = result.unwrap(); - expect(info.price).toBeNull(); - expect(info.state.getValue()).toBe(CheckoutStateEnum.UNKNOWN); - }); - - it('Given malformed price text, When extracting, Then price is null but state is detected', async () => { - const buttonHtml = 'Invalid'; - - mockLocator.getAttribute.mockResolvedValue('btn btn-success'); - mockLocator.innerHTML.mockResolvedValue(buttonHtml); - mockPillLocator.textContent.mockResolvedValue('Invalid'); - - const extractor = new CheckoutPriceExtractor(mockPage); - const result = await extractor.extractCheckoutInfo(); - - expect(result.isOk()).toBe(true); - const info = result.unwrap(); - expect(info.price).toBeNull(); - expect(info.state.getValue()).toBe(CheckoutStateEnum.READY); - }); - }); -}); \ No newline at end of file diff --git a/tests/integration/infrastructure/InMemorySessionRepository.test.ts b/tests/integration/infrastructure/InMemorySessionRepository.test.ts deleted file mode 100644 index 15521bc25..000000000 --- a/tests/integration/infrastructure/InMemorySessionRepository.test.ts +++ /dev/null @@ -1,365 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { InMemorySessionRepository } from '../../../apps/companion/main/automation/infrastructure/repositories/InMemorySessionRepository'; -import { AutomationSession } from 'apps/companion/main/automation/domain/entities/AutomationSession'; -import { StepId } from 'apps/companion/main/automation/domain/value-objects/StepId'; - -describe('InMemorySessionRepository Integration Tests', () => { - let repository: InMemorySessionRepository; - - beforeEach(() => { - repository = new InMemorySessionRepository(); - }); - - describe('save', () => { - it('should persist a new session', async () => { - const session = AutomationSession.create({ - sessionName: 'Test Race', - trackId: 'spa', - carIds: ['dallara-f3'], - }); - - await repository.save(session); - - const retrieved = await repository.findById(session.id); - expect(retrieved).toBeDefined(); - expect(retrieved?.id).toBe(session.id); - }); - - it('should update existing session on duplicate save', async () => { - const session = AutomationSession.create({ - sessionName: 'Test Race', - trackId: 'spa', - carIds: ['dallara-f3'], - }); - - await repository.save(session); - - session.start(); - session.transitionToStep(StepId.create(2)); - - await repository.save(session); - - const retrieved = await repository.findById(session.id); - expect(retrieved?.currentStep.value).toBe(2); - expect(retrieved?.state.isInProgress()).toBe(true); - }); - - it('should preserve all session properties', async () => { - const session = AutomationSession.create({ - sessionName: 'Test Race Session', - trackId: 'spa-francorchamps', - carIds: ['dallara-f3', 'porsche-911-gt3'], - }); - - await repository.save(session); - - const retrieved = await repository.findById(session.id); - expect(retrieved?.config.sessionName).toBe('Test Race Session'); - expect(retrieved?.config.trackId).toBe('spa-francorchamps'); - expect(retrieved?.config.carIds).toEqual(['dallara-f3', 'porsche-911-gt3']); - }); - }); - - describe('findById', () => { - it('should return null for non-existent session', async () => { - const result = await repository.findById('non-existent-id'); - - expect(result).toBeNull(); - }); - - it('should retrieve existing session by ID', async () => { - const session = AutomationSession.create({ - sessionName: 'Test Race', - trackId: 'spa', - carIds: ['dallara-f3'], - }); - - await repository.save(session); - - const retrieved = await repository.findById(session.id); - expect(retrieved).toBeDefined(); - expect(retrieved?.id).toBe(session.id); - }); - - it('should return domain entity not DTO', async () => { - const session = AutomationSession.create({ - sessionName: 'Test Race', - trackId: 'spa', - carIds: ['dallara-f3'], - }); - - await repository.save(session); - - const retrieved = await repository.findById(session.id); - expect(retrieved).toBeInstanceOf(AutomationSession); - }); - - it('should retrieve session with correct state', async () => { - const session = AutomationSession.create({ - sessionName: 'Test Race', - trackId: 'spa', - carIds: ['dallara-f3'], - }); - session.start(); - - await repository.save(session); - - const retrieved = await repository.findById(session.id); - expect(retrieved?.state.isInProgress()).toBe(true); - expect(retrieved?.startedAt).toBeDefined(); - }); - }); - - describe('update', () => { - it('should update existing session', async () => { - const session = AutomationSession.create({ - sessionName: 'Test Race', - trackId: 'spa', - carIds: ['dallara-f3'], - }); - - await repository.save(session); - - session.start(); - session.transitionToStep(StepId.create(2)); - - await repository.update(session); - - const retrieved = await repository.findById(session.id); - expect(retrieved?.currentStep.value).toBe(2); - }); - - it('should throw error when updating non-existent session', async () => { - const session = AutomationSession.create({ - sessionName: 'Test Race', - trackId: 'spa', - carIds: ['dallara-f3'], - }); - - await expect(repository.update(session)).rejects.toThrow('Session not found'); - }); - - it('should preserve unchanged properties', async () => { - const session = AutomationSession.create({ - sessionName: 'Original Name', - trackId: 'spa', - carIds: ['dallara-f3'], - }); - - await repository.save(session); - - session.start(); - - await repository.update(session); - - const retrieved = await repository.findById(session.id); - expect(retrieved?.config.sessionName).toBe('Original Name'); - expect(retrieved?.state.isInProgress()).toBe(true); - }); - - it('should update session state correctly', async () => { - const session = AutomationSession.create({ - sessionName: 'Test Race', - trackId: 'spa', - carIds: ['dallara-f3'], - }); - - await repository.save(session); - - session.start(); - session.pause(); - - await repository.update(session); - - const retrieved = await repository.findById(session.id); - expect(retrieved?.state.value).toBe('PAUSED'); - }); - }); - - describe('delete', () => { - it('should remove session from storage', async () => { - const session = AutomationSession.create({ - sessionName: 'Test Race', - trackId: 'spa', - carIds: ['dallara-f3'], - }); - - await repository.save(session); - await repository.delete(session.id); - - const retrieved = await repository.findById(session.id); - expect(retrieved).toBeNull(); - }); - - it('should not throw when deleting non-existent session', async () => { - await expect(repository.delete('non-existent-id')).resolves.not.toThrow(); - }); - - it('should only delete specified session', async () => { - const session1 = AutomationSession.create({ - sessionName: 'Race 1', - trackId: 'spa', - carIds: ['dallara-f3'], - }); - - const session2 = AutomationSession.create({ - sessionName: 'Race 2', - trackId: 'monza', - carIds: ['porsche-911-gt3'], - }); - - await repository.save(session1); - await repository.save(session2); - - await repository.delete(session1.id); - - const retrieved1 = await repository.findById(session1.id); - const retrieved2 = await repository.findById(session2.id); - - expect(retrieved1).toBeNull(); - expect(retrieved2).toBeDefined(); - }); - }); - - describe('findAll', () => { - it('should return empty array when no sessions exist', async () => { - const sessions = await repository.findAll(); - - expect(sessions).toEqual([]); - }); - - it('should return all saved sessions', async () => { - const session1 = AutomationSession.create({ - sessionName: 'Race 1', - trackId: 'spa', - carIds: ['dallara-f3'], - }); - - const session2 = AutomationSession.create({ - sessionName: 'Race 2', - trackId: 'monza', - carIds: ['porsche-911-gt3'], - }); - - await repository.save(session1); - await repository.save(session2); - - const sessions = await repository.findAll(); - - expect(sessions).toHaveLength(2); - expect(sessions.map(s => s.id)).toContain(session1.id); - expect(sessions.map(s => s.id)).toContain(session2.id); - }); - - it('should return domain entities not DTOs', async () => { - const session = AutomationSession.create({ - sessionName: 'Test Race', - trackId: 'spa', - carIds: ['dallara-f3'], - }); - - await repository.save(session); - - const sessions = await repository.findAll(); - - expect(sessions[0]).toBeInstanceOf(AutomationSession); - }); - }); - - describe('findByState', () => { - it('should return sessions matching state', async () => { - const session1 = AutomationSession.create({ - sessionName: 'Race 1', - trackId: 'spa', - carIds: ['dallara-f3'], - }); - session1.start(); - - const session2 = AutomationSession.create({ - sessionName: 'Race 2', - trackId: 'monza', - carIds: ['porsche-911-gt3'], - }); - - await repository.save(session1); - await repository.save(session2); - - const inProgressSessions = await repository.findByState('IN_PROGRESS'); - - expect(inProgressSessions).toHaveLength(1); - expect(inProgressSessions[0]!.id).toBe(session1.id); - }); - - it('should return empty array when no sessions match state', async () => { - const session = AutomationSession.create({ - sessionName: 'Test Race', - trackId: 'spa', - carIds: ['dallara-f3'], - }); - - await repository.save(session); - - const completedSessions = await repository.findByState('COMPLETED'); - - expect(completedSessions).toEqual([]); - }); - - it('should handle multiple sessions with same state', async () => { - const session1 = AutomationSession.create({ - sessionName: 'Race 1', - trackId: 'spa', - carIds: ['dallara-f3'], - }); - - const session2 = AutomationSession.create({ - sessionName: 'Race 2', - trackId: 'monza', - carIds: ['porsche-911-gt3'], - }); - - await repository.save(session1); - await repository.save(session2); - - const pendingSessions = await repository.findByState('PENDING'); - - expect(pendingSessions).toHaveLength(2); - }); - }); - - describe('concurrent operations', () => { - it('should handle concurrent saves', async () => { - const sessions = Array.from({ length: 10 }, (_, i) => - AutomationSession.create({ - sessionName: `Race ${i}`, - trackId: 'spa', - carIds: ['dallara-f3'], - }) - ); - - await Promise.all(sessions.map(s => repository.save(s))); - - const allSessions = await repository.findAll(); - expect(allSessions).toHaveLength(10); - }); - - it('should handle concurrent updates', async () => { - const session = AutomationSession.create({ - sessionName: 'Test Race', - trackId: 'spa', - carIds: ['dallara-f3'], - }); - - await repository.save(session); - session.start(); - - await Promise.all([ - repository.update(session), - repository.update(session), - repository.update(session), - ]); - - const retrieved = await repository.findById(session.id); - expect(retrieved?.state.isInProgress()).toBe(true); - }); - }); -}); \ No newline at end of file diff --git a/tests/integration/infrastructure/MockBrowserAutomationAdapter.test.ts b/tests/integration/infrastructure/MockBrowserAutomationAdapter.test.ts deleted file mode 100644 index 7dacd66f3..000000000 --- a/tests/integration/infrastructure/MockBrowserAutomationAdapter.test.ts +++ /dev/null @@ -1,282 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; -import { MockBrowserAutomationAdapter } from 'core/automation/infrastructure//automation'; -import { StepId } from 'apps/companion/main/automation/domain/value-objects/StepId'; - -describe('MockBrowserAutomationAdapter Integration Tests', () => { - let adapter: MockBrowserAutomationAdapter; - - beforeEach(() => { - adapter = new MockBrowserAutomationAdapter(); - }); - - describe('navigateToPage', () => { - it('should simulate navigation with delay', async () => { - const url = 'https://members.iracing.com/membersite/HostedRacing'; - - const result = await adapter.navigateToPage(url); - - expect(result.success).toBe(true); - expect(result.loadTime).toBeGreaterThan(0); - }); - - it('should return navigation URL in result', async () => { - const url = 'https://members.iracing.com/membersite/HostedRacing'; - - const result = await adapter.navigateToPage(url); - - expect(result.url).toBe(url); - }); - - it('should simulate realistic delays', async () => { - const url = 'https://members.iracing.com/membersite/HostedRacing'; - - const result = await adapter.navigateToPage(url); - - expect(result.loadTime).toBeGreaterThanOrEqual(200); - expect(result.loadTime).toBeLessThanOrEqual(800); - }); - }); - - describe('fillFormField', () => { - it('should simulate form field fill with delay', async () => { - const fieldName = 'session-name'; - const value = 'Test Race Session'; - - const result = await adapter.fillFormField(fieldName, value); - - expect(result.success).toBe(true); - expect(result.fieldName).toBe(fieldName); - expect(result.valueSet).toBe(value); - }); - - it('should simulate typing speed delay', async () => { - const fieldName = 'session-name'; - const value = 'A'.repeat(50); - - const result = await adapter.fillFormField(fieldName, value); - - expect(result.valueSet).toBeDefined(); - }); - - it('should handle empty field values', async () => { - const fieldName = 'session-name'; - const value = ''; - - const result = await adapter.fillFormField(fieldName, value); - - expect(result.success).toBe(true); - expect(result.valueSet).toBe(''); - }); - }); - - describe('clickElement', () => { - it('should simulate button click with delay', async () => { - const selector = '#create-session-button'; - - const result = await adapter.clickElement(selector); - - expect(result.success).toBe(true); - expect(result.target).toBe(selector); - }); - - it('should simulate click delays', async () => { - const selector = '#submit-button'; - - const result = await adapter.clickElement(selector); - - expect(result.target).toBeDefined(); - }); - }); - - describe('waitForElement', () => { - it('should simulate waiting for element to appear', async () => { - const selector = '.modal-dialog'; - - const result = await adapter.waitForElement(selector); - - expect(result.success).toBe(true); - expect(result.target).toBe(selector); - }); - - it('should simulate element load time', async () => { - const selector = '.loading-spinner'; - - const result = await adapter.waitForElement(selector); - - expect(result.waitedMs).toBeGreaterThanOrEqual(100); - expect(result.waitedMs).toBeLessThanOrEqual(1000); - }); - - it('should timeout after maximum wait time', async () => { - const selector = '.non-existent-element'; - const maxWaitMs = 5000; - - const result = await adapter.waitForElement(selector, maxWaitMs); - expect(result.success).toBe(true); - }); - }); - - describe('handleModal', () => { - it('should simulate modal handling for step 6', async () => { - const stepId = StepId.create(6); - const action = 'close'; - - const result = await adapter.handleModal(stepId, action); - - expect(result.success).toBe(true); - expect(result.stepId).toBe(6); - expect(result.action).toBe(action); - }); - - it('should simulate modal handling for step 9', async () => { - const stepId = StepId.create(9); - const action = 'confirm'; - - const result = await adapter.handleModal(stepId, action); - - expect(result.success).toBe(true); - expect(result.stepId).toBe(9); - }); - - it('should simulate modal handling for step 12', async () => { - const stepId = StepId.create(12); - const action = 'select'; - - const result = await adapter.handleModal(stepId, action); - - expect(result.success).toBe(true); - expect(result.stepId).toBe(12); - }); - - it('should throw error for non-modal steps', async () => { - const stepId = StepId.create(1); - const action = 'close'; - - await expect(adapter.handleModal(stepId, action)).rejects.toThrow( - 'Step 1 is not a modal step' - ); - }); - - it('should simulate modal interaction delays', async () => { - const stepId = StepId.create(6); - const action = 'close'; - - const result = await adapter.handleModal(stepId, action); - - expect(result.success).toBe(true); - expect(result.stepId).toBe(6); - expect(result.action).toBe(action); - }); - }); - - describe('executeStep', () => { - it('should execute step 1 (navigation)', async () => { - const stepId = StepId.create(1); - const config = { - sessionName: 'Test Race', - trackId: 'spa', - carIds: ['dallara-f3'], - }; - - const result = await adapter.executeStep(stepId, config); - - expect(result.success).toBe(true); - expect(result.metadata?.stepId).toBe(1); - }); - - it('should execute step 6 (modal step)', async () => { - const stepId = StepId.create(6); - const config = { - sessionName: 'Test Race', - trackId: 'spa', - carIds: ['dallara-f3'], - }; - - const result = await adapter.executeStep(stepId, config); - - expect(result.success).toBe(true); - expect(result.metadata?.stepId).toBe(6); - expect(result.metadata?.wasModalStep).toBe(true); - }); - - it('should execute step 17 (final step)', async () => { - const stepId = StepId.create(17); - const config = { - sessionName: 'Test Race', - trackId: 'spa', - carIds: ['dallara-f3'], - }; - - const result = await adapter.executeStep(stepId, config); - - expect(result.success).toBe(true); - expect(result.metadata?.stepId).toBe(17); - expect(result.metadata?.shouldStop).toBe(true); - }); - - it('should simulate realistic step execution times', async () => { - const stepId = StepId.create(5); - const config = { - sessionName: 'Test Race', - trackId: 'spa', - carIds: ['dallara-f3'], - }; - - const result = await adapter.executeStep(stepId, config); - - expect(result.metadata?.executionTime).toBeGreaterThan(0); - }); - }); - - describe('error simulation', () => { - it('should simulate random failures when enabled', async () => { - const adapterWithFailures = new MockBrowserAutomationAdapter({ - simulateFailures: true, - failureRate: 1.0, // Always fail - }); - - const stepId = StepId.create(5); - const config = { - sessionName: 'Test Race', - trackId: 'spa', - carIds: ['dallara-f3'], - }; - - await expect(adapterWithFailures.executeStep(stepId, config)).rejects.toThrow(); - }); - - it('should not fail when failure simulation disabled', async () => { - const adapterNoFailures = new MockBrowserAutomationAdapter({ - simulateFailures: false, - }); - - const stepId = StepId.create(5); - const config = { - sessionName: 'Test Race', - trackId: 'spa', - carIds: ['dallara-f3'], - }; - - const result = await adapterNoFailures.executeStep(stepId, config); - - expect(result.success).toBe(true); - }); - }); - - describe('performance metrics', () => { - it('should track operation metrics', async () => { - const stepId = StepId.create(1); - const config = { - sessionName: 'Test Race', - trackId: 'spa', - carIds: ['dallara-f3'], - }; - - const result = await adapter.executeStep(stepId, config); - - expect(result.metadata).toBeDefined(); - expect(result.metadata?.totalDelay).toBeGreaterThan(0); - expect(result.metadata?.operationCount).toBeGreaterThan(0); - }); - }); -}); \ No newline at end of file diff --git a/tests/integration/infrastructure/automation/OverlayLifecycle.integration.test.ts b/tests/integration/infrastructure/automation/OverlayLifecycle.integration.test.ts deleted file mode 100644 index fe942ca27..000000000 --- a/tests/integration/infrastructure/automation/OverlayLifecycle.integration.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { OverlaySyncService } from 'apps/companion/main/automation/application/services/OverlaySyncService'; -import type { AutomationEvent } from 'apps/companion/main/automation/application/ports/IAutomationEventPublisher'; -import type { - IAutomationLifecycleEmitter, - LifecycleCallback, -} from 'core/automation/infrastructure//IAutomationLifecycleEmitter'; -import type { - OverlayAction, - ActionAck, -} from 'apps/companion/main/automation/application/ports/IOverlaySyncPort'; - -class TestLifecycleEmitter implements IAutomationLifecycleEmitter { - private callbacks: Set = new Set(); - - onLifecycle(cb: LifecycleCallback): void { - this.callbacks.add(cb); - } - - offLifecycle(cb: LifecycleCallback): void { - this.callbacks.delete(cb); - } - - async emit(event: AutomationEvent): Promise { - for (const cb of Array.from(this.callbacks)) { - await cb(event); - } - } -} - -class RecordingPublisher { - public events: AutomationEvent[] = []; - - async publish(event: AutomationEvent): Promise { - this.events.push(event); - } -} - -describe('Overlay lifecycle (integration)', () => { - it('emits modal-opened and confirms after action-started in sane order', async () => { - const lifecycleEmitter = new TestLifecycleEmitter(); - const publisher = new RecordingPublisher(); - type LoggerLike = { - debug: (...args: unknown[]) => void; - info: (...args: unknown[]) => void; - warn: (...args: unknown[]) => void; - error: (...args: unknown[]) => void; - fatal: (...args: unknown[]) => void; - child: (...args: unknown[]) => LoggerLike; - flush: (...args: unknown[]) => Promise; - }; - const logger = console as unknown as LoggerLike; - - const service = new OverlaySyncService({ - lifecycleEmitter, - publisher, - logger, - defaultTimeoutMs: 1_000, - }); - - const action: OverlayAction = { - id: 'hosted-session', - label: 'Starting hosted session', - }; - - const ackPromise: Promise = service.startAction(action); - - expect(publisher.events.length).toBe(1); - const first = publisher.events[0]!; - expect(first.type).toBe('modal-opened'); - expect(first.actionId).toBe('hosted-session'); - - await lifecycleEmitter.emit({ - type: 'panel-attached', - actionId: 'hosted-session', - timestamp: Date.now(), - payload: { selector: '#gridpilot-overlay' }, - }); - - await lifecycleEmitter.emit({ - type: 'action-started', - actionId: 'hosted-session', - timestamp: Date.now(), - }); - - const ack = await ackPromise; - expect(ack.id).toBe('hosted-session'); - expect(ack.status).toBe('confirmed'); - - expect(publisher.events[0]!.type).toBe('modal-opened'); - expect(publisher.events[0]!.actionId).toBe('hosted-session'); - }); - - it('emits panel-missing when cancelAction is called', async () => { - const lifecycleEmitter = new TestLifecycleEmitter(); - const publisher = new RecordingPublisher(); - type LoggerLike = { - debug: (...args: unknown[]) => void; - info: (...args: unknown[]) => void; - warn: (...args: unknown[]) => void; - error: (...args: unknown[]) => void; - fatal: (...args: unknown[]) => void; - child: (...args: unknown[]) => LoggerLike; - flush: (...args: unknown[]) => Promise; - }; - const logger = console as unknown as LoggerLike; - - const service = new OverlaySyncService({ - lifecycleEmitter, - publisher, - logger, - }); - - await service.cancelAction('hosted-session-cancel'); - - expect(publisher.events.length).toBe(1); - const ev = publisher.events[0]!; - expect(ev.type).toBe('panel-missing'); - expect(ev.actionId).toBe('hosted-session-cancel'); - }); -}); \ No newline at end of file diff --git a/tests/integration/infrastructure/automation/ValidatorConformance.integration.test.ts b/tests/integration/infrastructure/automation/ValidatorConformance.integration.test.ts deleted file mode 100644 index 7fcddc009..000000000 --- a/tests/integration/infrastructure/automation/ValidatorConformance.integration.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { PageStateValidator } from 'apps/companion/main/automation/domain/services/PageStateValidator'; -import { StepTransitionValidator } from 'apps/companion/main/automation/domain/services/StepTransitionValidator'; -import { StepId } from 'apps/companion/main/automation/domain/value-objects/StepId'; -import { SessionState } from 'apps/companion/main/automation/domain/value-objects/SessionState'; - -describe('Validator conformance (integration)', () => { - describe('PageStateValidator with hosted-session selectors', () => { - it('reports missing DOM markers with descriptive message', () => { - const validator = new PageStateValidator(); - - const actualState = (selector: string) => { - return selector === '#set-cars'; - }; - - const result = validator.validateState(actualState, { - expectedStep: 'track', - requiredSelectors: ['#set-track', '#track-search'], - }); - - expect(result.isOk()).toBe(true); - const value = result.unwrap(); - expect(value.isValid).toBe(false); - expect(value.expectedStep).toBe('track'); - expect(value.missingSelectors).toEqual(['#set-track', '#track-search']); - expect(value.message).toBe( - 'Page state mismatch: Expected to be on "track" page but missing required elements', - ); - }); - - it('reports unexpected DOM markers when forbidden selectors are present', () => { - const validator = new PageStateValidator(); - - const actualState = (selector: string) => { - return ['#set-cars', '#set-track'].includes(selector); - }; - - const result = validator.validateState(actualState, { - expectedStep: 'cars', - requiredSelectors: ['#set-cars'], - forbiddenSelectors: ['#set-track'], - }); - - expect(result.isOk()).toBe(true); - const value = result.unwrap(); - expect(value.isValid).toBe(false); - expect(value.expectedStep).toBe('cars'); - expect(value.unexpectedSelectors).toEqual(['#set-track']); - expect(value.message).toBe( - 'Page state mismatch: Found unexpected elements on "cars" page', - ); - }); - }); - - describe('StepTransitionValidator with hosted-session steps', () => { - it('rejects illegal forward jumps with clear error', () => { - const currentStep = StepId.create(3); - const nextStep = StepId.create(9); - const state = SessionState.create('IN_PROGRESS'); - - const result = StepTransitionValidator.canTransition( - currentStep, - nextStep, - state, - ); - - expect(result.isValid).toBe(false); - expect(result.error).toBe( - 'Cannot skip steps - must progress sequentially', - ); - }); - - it('rejects backward jumps with clear error', () => { - const currentStep = StepId.create(11); - const nextStep = StepId.create(8); - const state = SessionState.create('IN_PROGRESS'); - - const result = StepTransitionValidator.canTransition( - currentStep, - nextStep, - state, - ); - - expect(result.isValid).toBe(false); - expect(result.error).toBe( - 'Cannot move backward - steps must progress forward only', - ); - }); - - it('provides descriptive step descriptions for hosted steps', () => { - const step3 = StepTransitionValidator.getStepDescription( - StepId.create(3), - ); - const step11 = StepTransitionValidator.getStepDescription( - StepId.create(11), - ); - const finalStep = StepTransitionValidator.getStepDescription( - StepId.create(17), - ); - - expect(step3).toBe('Fill Race Information'); - expect(step11).toBe('Set Track'); - expect(finalStep).toBe( - 'Track Conditions (STOP - Manual Submit Required)', - ); - }); - }); -}); \ No newline at end of file diff --git a/tests/integration/interface/companion/companion-start-automation.browser-mode-refresh.integration.test.ts b/tests/integration/interface/companion/companion-start-automation.browser-mode-refresh.integration.test.ts deleted file mode 100644 index ee81d0963..000000000 --- a/tests/integration/interface/companion/companion-start-automation.browser-mode-refresh.integration.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { DIContainer } from '../../../..//apps/companion/main/di-container'; -import type { HostedSessionConfig } from 'apps/companion/main/automation/domain/types/HostedSessionConfig'; -import { StepId } from 'apps/companion/main/automation/domain/value-objects/StepId'; -import { PlaywrightAutomationAdapter } from '../../../../apps/companion/main/automation/infrastructure/automation'; - -describe('companion start automation - browser mode refresh wiring', () => { - const originalEnv = { ...process.env }; - let originalTestLauncher: unknown; - - beforeEach(() => { - process.env = { ...originalEnv, NODE_ENV: 'development' }; - - originalTestLauncher = (PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & { - testLauncher?: unknown; - }).testLauncher; - - const mockLauncher = { - launch: async (_opts: unknown) => ({ - newContext: async () => ({ - newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }), - close: async () => {}, - }), - newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }), - close: async () => {}, - }), - launchPersistentContext: async (_userDataDir: string, _opts: unknown) => ({ - pages: () => [{ setDefaultTimeout: () => {}, close: async () => {} }], - newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }), - close: async () => {}, - }), - }; - - (PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & { - testLauncher?: typeof mockLauncher; - }).testLauncher = mockLauncher; - - DIContainer.resetInstance(); - }); - - afterEach(async () => { - const container = DIContainer.getInstance(); - await container.shutdown(); - DIContainer.resetInstance(); - (PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & { - testLauncher?: unknown; - }).testLauncher = originalTestLauncher; - process.env = originalEnv; - }); - - it('uses refreshed browser automation for connection and step execution after mode change', async () => { - const container = DIContainer.getInstance(); - - const loader = container.getBrowserModeConfigLoader(); - expect(loader.getDevelopmentMode()).toBe('headed'); - - const preStart = container.getStartAutomationUseCase(); - const preEngine = container.getAutomationEngine(); - const preAutomation = container.getBrowserAutomation(); - - expect(preAutomation).toBe(preEngine.browserAutomation); - - loader.setDevelopmentMode('headless'); - container.refreshBrowserAutomation(); - - const postStart = container.getStartAutomationUseCase(); - const postEngine = container.getAutomationEngine(); - const postAutomation = container.getBrowserAutomation(); - - expect(postAutomation).toBe(postEngine.browserAutomation); - expect(postAutomation).not.toBe(preAutomation); - expect(postStart).not.toBe(preStart); - - const connectionResult = await container.initializeBrowserConnection(); - expect(connectionResult.success).toBe(true); - - const config: HostedSessionConfig = { - sessionName: 'Companion browser-mode refresh wiring', - trackId: 'test-track', - carIds: ['car-1'], - }; - - const dto = await postStart.execute(config); - - await postEngine.executeStep(StepId.create(1), config); - - const sessionRepository = container.getSessionRepository(); - const session = await sessionRepository.findById(dto.sessionId); - - expect(session).toBeDefined(); - - const state = session!.state.value as string; - const errorMessage = session!.errorMessage as string | undefined; - - if (errorMessage) { - expect(errorMessage).not.toContain('Browser not connected'); - } - - const automationFromConnection = container.getBrowserAutomation(); - const automationFromEngine = (container.getAutomationEngine() as { browserAutomation: unknown }) - .browserAutomation; - - expect(automationFromConnection).toBe(automationFromEngine); - expect(automationFromConnection).toBe(postAutomation); - }); -}); \ No newline at end of file diff --git a/tests/integration/interface/companion/companion-start-automation.browser-not-connected.integration.test.ts b/tests/integration/interface/companion/companion-start-automation.browser-not-connected.integration.test.ts deleted file mode 100644 index 6313cac4f..000000000 --- a/tests/integration/interface/companion/companion-start-automation.browser-not-connected.integration.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { DIContainer } from '../../../..//apps/companion/main/di-container'; -import type { HostedSessionConfig } from 'apps/companion/main/automation/domain/types/HostedSessionConfig'; -import { StepId } from 'apps/companion/main/automation/domain/value-objects/StepId'; -import { PlaywrightAutomationAdapter } from '../../../../apps/companion/main/automation/infrastructure/automation'; - -describe('companion start automation - browser not connected at step 1', () => { - const originalEnv = { ...process.env }; - let originalTestLauncher: unknown; - - beforeEach(() => { - process.env = { ...originalEnv, NODE_ENV: 'production' }; - - originalTestLauncher = (PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & { - testLauncher?: unknown; - }).testLauncher; - - const mockLauncher = { - launch: async (_opts: unknown) => ({ - newContext: async () => ({ - newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }), - close: async () => {}, - }), - newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }), - close: async () => {}, - }), - launchPersistentContext: async (_userDataDir: string, _opts: unknown) => ({ - pages: () => [{ setDefaultTimeout: () => {}, close: async () => {} }], - newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }), - close: async () => {}, - }), - }; - - (PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & { - testLauncher?: typeof mockLauncher; - }).testLauncher = mockLauncher; - - DIContainer.resetInstance(); - }); - - afterEach(async () => { - const container = DIContainer.getInstance(); - await container.shutdown(); - DIContainer.resetInstance(); - (PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & { - testLauncher?: unknown; - }).testLauncher = originalTestLauncher; - process.env = originalEnv; - }); - - it('marks the session as FAILED with Step 1 (LOGIN) browser-not-connected error', async () => { - const container = DIContainer.getInstance(); - const startAutomationUseCase = container.getStartAutomationUseCase(); - const sessionRepository = container.getSessionRepository(); - const automationEngine = container.getAutomationEngine(); - - const connectionResult = await container.initializeBrowserConnection(); - expect(connectionResult.success).toBe(true); - - const browserAutomation = container.getBrowserAutomation(); - if (typeof (browserAutomation as { disconnect?: () => Promise }).disconnect === 'function') { - await (browserAutomation as { disconnect: () => Promise }).disconnect(); - } - - const config: HostedSessionConfig = { - sessionName: 'Companion integration browser-not-connected', - trackId: 'test-track', - carIds: ['car-1'], - }; - - const dto = await startAutomationUseCase.execute(config); - - await automationEngine.executeStep(StepId.create(1), config); - - const session = await waitForFailedSession(sessionRepository, dto.sessionId); - expect(session).toBeDefined(); - expect(session!.state!.value).toBe('FAILED'); - const error = session!.errorMessage as string | undefined; - expect(error).toBeDefined(); - expect(error).toContain('Step 1 (Navigate to Hosted Racing page)'); - expect(error).toContain('Browser not connected'); - }); -}); - -async function waitForFailedSession( - sessionRepository: { findById: (id: string) => Promise<{ state?: { value?: string }; errorMessage?: unknown } | null> }, - sessionId: string, - timeoutMs = 5000, -): Promise<{ state?: { value?: string }; errorMessage?: unknown } | null> { - const start = Date.now(); - let last: any = null; - - // eslint-disable-next-line no-constant-condition - while (true) { - last = await sessionRepository.findById(sessionId); - if (last && last.state && last.state.value === 'FAILED') { - return last; - } - if (Date.now() - start >= timeoutMs) { - return last; - } - await new Promise((resolve) => setTimeout(resolve, 100)); - } -} \ No newline at end of file diff --git a/tests/integration/interface/companion/companion-start-automation.connection-failure.integration.test.ts b/tests/integration/interface/companion/companion-start-automation.connection-failure.integration.test.ts deleted file mode 100644 index 0fc1059fc..000000000 --- a/tests/integration/interface/companion/companion-start-automation.connection-failure.integration.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; -import { DIContainer } from '../../../..//apps/companion/main/di-container'; -import type { HostedSessionConfig } from 'apps/companion/main/automation/domain/types/HostedSessionConfig'; -import { PlaywrightAutomationAdapter } from '../../../../apps/companion/main/automation/infrastructure/automation'; - -describe('companion start automation - browser connection failure before steps', () => { - const originalEnv = { ...process.env }; - let originalTestLauncher: unknown; - - beforeEach(() => { - process.env = { ...originalEnv, NODE_ENV: 'production' }; - - originalTestLauncher = (PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & { - testLauncher?: unknown; - }).testLauncher; - - const failingLauncher = { - launch: async () => { - throw new Error('Simulated browser launch failure'); - }, - launchPersistentContext: async () => { - throw new Error('Simulated persistent context failure'); - }, - }; - - (PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & { - testLauncher?: typeof failingLauncher; - }).testLauncher = failingLauncher; - - DIContainer.resetInstance(); - }); - - afterEach(async () => { - const container = DIContainer.getInstance(); - await container.shutdown(); - DIContainer.resetInstance(); - (PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & { - testLauncher?: unknown; - }).testLauncher = originalTestLauncher; - process.env = originalEnv; - }); - - it('fails browser connection and aborts before executing step 1', async () => { - const container = DIContainer.getInstance(); - const startAutomationUseCase = container.getStartAutomationUseCase(); - const sessionRepository = container.getSessionRepository(); - const automationEngine = container.getAutomationEngine(); - - const connectionResult = await container.initializeBrowserConnection(); - expect(connectionResult.success).toBe(false); - expect(connectionResult.error).toBeDefined(); - - const executeStepSpy = vi.spyOn( - automationEngine, - 'executeStep', - ); - - const config: HostedSessionConfig = { - sessionName: 'Companion integration connection failure', - trackId: 'test-track', - carIds: ['car-1'], - }; - - let sessionId: string | null = null; - - try { - const dto = await startAutomationUseCase.execute(config); - sessionId = dto.sessionId; - } catch (error) { - expect((error as Error).message).toBeDefined(); - } - - expect(executeStepSpy).not.toHaveBeenCalled(); - - if (sessionId) { - const session = await sessionRepository.findById(sessionId); - if (session) { - const message = session.errorMessage as string | undefined; - if (message) { - expect(message).not.toContain('Step 1 (LOGIN) failed: Browser not connected'); - expect(message.toLowerCase()).toContain('browser'); - } - } - } - }); - - it('treats successful adapter connect without a page as connection failure', async () => { - const container = DIContainer.getInstance(); - const browserAutomation = container.getBrowserAutomation(); - - expect(browserAutomation).toBeInstanceOf(PlaywrightAutomationAdapter); - - const AdapterWithPrototype = PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & { - prototype: { - connect: () => Promise<{ success: boolean; error?: string }>; - }; - }; - const originalConnect = AdapterWithPrototype.prototype.connect; - - AdapterWithPrototype.prototype.connect = async function () { - return { success: true }; - }; - - try { - const connectionResult = await container.initializeBrowserConnection(); - expect(connectionResult.success).toBe(false); - expect(connectionResult.error).toBeDefined(); - expect(String(connectionResult.error).toLowerCase()).toContain('browser'); - } finally { - AdapterWithPrototype.prototype.connect = originalConnect; - } - }); -}); \ No newline at end of file diff --git a/tests/integration/interface/companion/companion-start-automation.happy.integration.test.ts b/tests/integration/interface/companion/companion-start-automation.happy.integration.test.ts deleted file mode 100644 index 5e06e2328..000000000 --- a/tests/integration/interface/companion/companion-start-automation.happy.integration.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { DIContainer } from '../../../..//apps/companion/main/di-container'; -import type { HostedSessionConfig } from 'apps/companion/main/automation/domain/types/HostedSessionConfig'; -import { StepId } from 'apps/companion/main/automation/domain/value-objects/StepId'; - -describe('companion start automation - happy path', () => { - const originalEnv = { ...process.env }; - - beforeEach(() => { - process.env = { ...originalEnv, NODE_ENV: 'test' }; - DIContainer.resetInstance(); - }); - - afterEach(async () => { - const container = DIContainer.getInstance(); - await container.shutdown(); - DIContainer.resetInstance(); - process.env = originalEnv; - }); - - it('creates a non-failed session and does not report browser-not-connected', async () => { - const container = DIContainer.getInstance(); - const startAutomationUseCase = container.getStartAutomationUseCase(); - const sessionRepository = container.getSessionRepository(); - const automationEngine = container.getAutomationEngine(); - - const connectionResult = await container.initializeBrowserConnection(); - expect(connectionResult.success).toBe(true); - - const config: HostedSessionConfig = { - sessionName: 'Companion integration happy path', - trackId: 'test-track', - carIds: ['car-1'], - }; - - const dto = await startAutomationUseCase.execute(config); - - const sessionBefore = await sessionRepository.findById(dto.sessionId); - expect(sessionBefore).toBeDefined(); - - await automationEngine.executeStep(StepId.create(1), config); - - const session = await sessionRepository.findById(dto.sessionId); - expect(session).toBeDefined(); - - const state = session!.state.value as string; - expect(state).not.toBe('FAILED'); - - const errorMessage = session!.errorMessage as string | undefined; - if (errorMessage) { - expect(errorMessage).not.toContain('Browser not connected'); - } - }); -}); \ No newline at end of file diff --git a/tests/integration/interface/renderer/renderer-overlay-lifecycle.integration.test.ts b/tests/integration/interface/renderer/renderer-overlay-lifecycle.integration.test.ts deleted file mode 100644 index aa7e16a77..000000000 --- a/tests/integration/interface/renderer/renderer-overlay-lifecycle.integration.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { MockAutomationLifecycleEmitter } from '../../../mocks/MockAutomationLifecycleEmitter'; -import { OverlaySyncService } from 'apps/companion/main/automation/application/services/OverlaySyncService'; -import type { AutomationEvent } from 'apps/companion/main/automation/application/ports/IAutomationEventPublisher'; -import type { OverlayAction } from 'apps/companion/main/automation/application/ports/IOverlaySyncPort'; - -type RendererOverlayState = - | { status: 'idle' } - | { status: 'starting'; actionId: string } - | { status: 'in-progress'; actionId: string } - | { status: 'completed'; actionId: string } - | { status: 'failed'; actionId: string }; - -class RecordingPublisher { - public events: AutomationEvent[] = []; - async publish(event: AutomationEvent): Promise { - this.events.push(event); - } -} - -function reduceEventsToRendererState(events: AutomationEvent[]): RendererOverlayState { - let state: RendererOverlayState = { status: 'idle' }; - - for (const ev of events) { - if (!ev.actionId) continue; - switch (ev.type) { - case 'modal-opened': - case 'panel-attached': - state = { status: 'starting', actionId: ev.actionId }; - break; - case 'action-started': - state = { status: 'in-progress', actionId: ev.actionId }; - break; - case 'action-complete': - state = { status: 'completed', actionId: ev.actionId }; - break; - case 'action-failed': - case 'panel-missing': - state = { status: 'failed', actionId: ev.actionId }; - break; - } - } - - return state; -} - -describe('renderer overlay lifecycle integration', () => { - it('tracks starting → in-progress → completed lifecycle for a hosted action', async () => { - const emitter = new MockAutomationLifecycleEmitter(); - const publisher = new RecordingPublisher(); - const svc = new OverlaySyncService({ - lifecycleEmitter: emitter, - publisher, - logger: console as any, - defaultTimeoutMs: 2_000, - }); - - const action: OverlayAction = { - id: 'hosted-session', - label: 'Starting hosted session', - }; - - const ackPromise = svc.startAction(action); - - expect(publisher.events[0]?.type).toBe('modal-opened'); - expect(publisher.events[0]?.actionId).toBe('hosted-session'); - - await emitter.emit({ - type: 'panel-attached', - actionId: 'hosted-session', - timestamp: Date.now(), - payload: { selector: '#gridpilot-overlay' }, - }); - - await emitter.emit({ - type: 'action-started', - actionId: 'hosted-session', - timestamp: Date.now(), - }); - - const ack = await ackPromise; - expect(ack.id).toBe('hosted-session'); - expect(ack.status).toBe('confirmed'); - - await publisher.publish({ - type: 'panel-attached', - actionId: 'hosted-session', - timestamp: Date.now(), - payload: { selector: '#gridpilot-overlay' }, - } as AutomationEvent); - - await publisher.publish({ - type: 'action-started', - actionId: 'hosted-session', - timestamp: Date.now(), - } as AutomationEvent); - - await publisher.publish({ - type: 'action-complete', - actionId: 'hosted-session', - timestamp: Date.now(), - } as AutomationEvent); - - const rendererState = reduceEventsToRendererState(publisher.events); - - expect(rendererState.status).toBe('completed'); - expect((rendererState as { actionId: string }).actionId).toBe('hosted-session'); - }); - - it('ends in failed state when panel-missing is emitted', async () => { - const emitter = new MockAutomationLifecycleEmitter(); - const publisher = new RecordingPublisher(); - const svc = new OverlaySyncService({ - lifecycleEmitter: emitter, - publisher, - logger: console as any, - defaultTimeoutMs: 200, - }); - - const action: OverlayAction = { - id: 'hosted-failure', - label: 'Hosted session failing', - }; - - void svc.startAction(action); - - await publisher.publish({ - type: 'panel-attached', - actionId: 'hosted-failure', - timestamp: Date.now(), - payload: { selector: '#gridpilot-overlay' }, - } as AutomationEvent); - - await publisher.publish({ - type: 'action-failed', - actionId: 'hosted-failure', - timestamp: Date.now(), - payload: { reason: 'validation error' }, - } as AutomationEvent); - - const rendererState = reduceEventsToRendererState(publisher.events); - - expect(rendererState.status).toBe('failed'); - expect((rendererState as { actionId: string }).actionId).toBe('hosted-failure'); - }); -}); \ No newline at end of file diff --git a/tests/integration/interface/renderer/renderer-overlay.integration.test.ts b/tests/integration/interface/renderer/renderer-overlay.integration.test.ts deleted file mode 100644 index 442fc9ccf..000000000 --- a/tests/integration/interface/renderer/renderer-overlay.integration.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, expect, test } from 'vitest' -import { MockAutomationLifecycleEmitter } from '../../../mocks/MockAutomationLifecycleEmitter' -import { OverlaySyncService } from 'apps/companion/main/automation/application/services/OverlaySyncService' - -describe('renderer overlay integration', () => { - test('renderer shows confirmed only after main acks confirmed', async () => { - const emitter = new MockAutomationLifecycleEmitter() - const publisher: { publish: (event: unknown) => Promise } = { publish: async () => {} } - const svc = new OverlaySyncService({ - lifecycleEmitter: emitter, - publisher, - logger: console as any, - }) - - // simulate renderer request - const promise = svc.startAction({ id: 'add-car', label: 'Adding...' }) - - // ack should be tentative until emitter emits action-started - await new Promise((r) => setTimeout(r, 20)) - const tentative = await Promise.race([promise, Promise.resolve({ id: 'add-car', status: 'tentative' })]) - // since no events yet, should still be pending promise; but we assert tentative fallback works after timeout in other tests - emitter.emit({ type: 'action-started', actionId: 'add-car', timestamp: Date.now() }) - const ack = await promise - expect(ack.status).toBe('confirmed') - }) -}) \ No newline at end of file diff --git a/tests/smoke/browser-mode-toggle.smoke.test.ts b/tests/smoke/browser-mode-toggle.smoke.test.ts deleted file mode 100644 index f7a93b5e7..000000000 --- a/tests/smoke/browser-mode-toggle.smoke.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { test, expect } from 'vitest'; -import { DIContainer } from '@apps/companion/main/di-container'; - -test('renderer -> preload -> main: set/get updates BrowserModeConfigLoader (reproduces headless-toggle bug)', () => { - // Ensure environment is development so toggle is available - (process.env as any).NODE_ENV = 'development'; - - // Provide a minimal electron.app mock so DIContainer can resolve paths in node test environment - // This avoids calling the real Electron runtime during unit/runner tests. - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const electron = require('electron'); - electron.app = electron.app || {}; - electron.app.getAppPath = electron.app.getAppPath || (() => process.cwd()); - electron.app.isPackaged = electron.app.isPackaged || false; - electron.app.getPath = electron.app.getPath || ((p: string) => process.cwd()); - } catch { - // If require('electron') fails, ignore; DIContainer will still attempt to access app and may error. - } - - // Reset and get fresh DI container for test isolation - DIContainer.resetInstance(); - const container = DIContainer.getInstance(); - const loader = container.getBrowserModeConfigLoader(); - - // Sanity: toggle visible and default is 'headed' in development - expect(process.env.NODE_ENV).toBe('development'); - expect(loader.getDevelopmentMode()).toBe('headed'); - - // Simulate renderer setting to 'headless' via IPC (which should call loader.setDevelopmentMode) - loader.setDevelopmentMode('headless'); - - // After setting, the loader must reflect new value - expect(loader.getDevelopmentMode()).toBe('headless'); - - // loader.load() should report the GUI source in development and the updated mode - const config = loader.load(); - expect(config.mode).toBe('headless'); - expect(config.source).toBe('GUI'); -}); \ No newline at end of file diff --git a/tests/smoke/companion-boot.smoke.test.ts b/tests/smoke/companion-boot.smoke.test.ts deleted file mode 100644 index 89bdb6176..000000000 --- a/tests/smoke/companion-boot.smoke.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Experimental Playwright+Electron companion boot smoke test (retired). - * - * This suite attempted to launch the Electron-based companion app via - * Playwright's Electron driver, but it cannot run in this environment because - * Electron embeds Node.js 16.17.1 while the installed Playwright version - * requires Node.js 18 or higher. - * - * Companion behavior is instead covered by: - * - Playwright-based automation E2Es and integrations against fixtures. - * - Electron build/init/DI smoke tests. - * - Domain and application unit/integration tests. - * - * This file now contains a minimal Vitest suite to keep the historical - * entrypoint discoverable without failing the test runner. - */ -import { describe, it, expect } from 'vitest'; - -describe('companion-boot smoke (retired)', () => { - it('is documented as retired and covered elsewhere', () => { - expect(true).toBe(true); - }); -}); \ No newline at end of file diff --git a/tests/smoke/di-container.test.ts b/tests/smoke/di-container.test.ts deleted file mode 100644 index b956f9952..000000000 --- a/tests/smoke/di-container.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { DIContainer, resolveTemplatePath, resolveSessionDataPath } from '@apps/companion/main/di-container'; - -describe('DIContainer (smoke) - test-tolerance', () => { - it('constructs without electron.app and exposes path resolvers', () => { - // Constructing DIContainer should not throw in plain Node (vitest) environment. - expect(() => { - // Note: getInstance lazily constructs the container - DIContainer.resetInstance(); - DIContainer.getInstance(); - }).not.toThrow(); - - // Path resolvers should return strings - const tpl = resolveTemplatePath(); - const sess = resolveSessionDataPath(); - expect(typeof tpl).toBe('string'); - expect(tpl.length).toBeGreaterThan(0); - expect(typeof sess).toBe('string'); - expect(sess.length).toBeGreaterThan(0); - }); -}); \ No newline at end of file diff --git a/tests/smoke/electron-app.smoke.test.ts b/tests/smoke/electron-app.smoke.test.ts deleted file mode 100644 index 90999c6ef..000000000 --- a/tests/smoke/electron-app.smoke.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Legacy Electron app smoke suite (superseded). - * - * Canonical boot coverage now lives in - * [companion-boot.smoke.test.ts](tests/smoke/companion-boot.smoke.test.ts). - * - * This file now contains a minimal Vitest suite to keep the historical - * entrypoint discoverable without failing the test runner. - */ -import { describe, it, expect } from 'vitest'; - -describe('electron-app smoke (superseded)', () => { - it('is documented as superseded and covered elsewhere', () => { - expect(true).toBe(true); - }); -}); \ No newline at end of file diff --git a/tests/smoke/electron-build.smoke.test.ts b/tests/smoke/electron-build.smoke.test.ts deleted file mode 100644 index c409503aa..000000000 --- a/tests/smoke/electron-build.smoke.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { execSync } from 'child_process'; - -/** - * Electron Build Smoke Test - * - * Purpose: Detect browser context errors during Electron build. - * - * This test catches bundling issues where Node.js modules are imported - * in the renderer process, causing runtime errors. - * - * It now runs under the Playwright test runner used by the smoke suite. - */ - -test.describe('Electron Build Smoke Tests', () => { - test('should build Electron app without browser context errors', () => { - // When: Building the Electron companion app - let buildOutput: string; - - try { - buildOutput = execSync('npm run companion:build', { - cwd: process.cwd(), - encoding: 'utf-8', - stdio: 'pipe', - }); - } catch (error: any) { - buildOutput = error.stdout + error.stderr; - } - - // Then: Build should not contain externalized module warnings - const foundErrors: string[] = []; - - // Split output into lines and check each line - const lines = buildOutput.split('\n'); - lines.forEach((line: string) => { - if (line.includes('has been externalized for browser compatibility')) { - foundErrors.push(line.trim()); - } - if (line.includes('Cannot access') && line.includes('in client code')) { - foundErrors.push(line.trim()); - } - }); - - // This WILL FAIL in RED phase due to electron/fs/path being externalized - expect( - foundErrors.length, - `Browser context errors detected during build:\n\n${foundErrors.map((e, i) => `${i + 1}. ${e}`).join('\n')}\n\n` + - `These indicate Node.js modules (electron, fs, path) are being imported in renderer code.\n` + - `This will cause runtime errors when the app launches.` - ).toBe(0); - }); - - test('should not import Node.js modules in renderer source code', () => { - // Given: Renderer source code - const fs = require('fs'); - const path = require('path'); - - const rendererPath = path.join( - process.cwd(), - 'apps/companion/renderer' - ); - - // When: Checking renderer source for forbidden imports - const forbiddenPatterns = [ - { pattern: /from\s+['"]electron['"]/, name: 'electron' }, - { pattern: /require\(['"]electron['"]\)/, name: 'electron' }, - { pattern: /from\s+['"]fs['"]/, name: 'fs' }, - { pattern: /require\(['"]fs['"]\)/, name: 'fs' }, - { pattern: /from\s+['"]path['"]/, name: 'path' }, - { pattern: /require\(['"]path['"]\)/, name: 'path' }, - ]; - - const violations: Array<{ file: string; line: number; import: string; module: string }> = []; - - function scanDirectory(dir: string) { - const entries = fs.readdirSync(dir, { withFileTypes: true }); - - entries.forEach((entry: any) => { - const fullPath = path.join(dir, entry.name); - - if (entry.isDirectory()) { - scanDirectory(fullPath); - } else if (entry.name.endsWith('.tsx') || entry.name.endsWith('.ts')) { - const content = fs.readFileSync(fullPath, 'utf-8'); - const lines = content.split('\n'); - - lines.forEach((line: string, index: number) => { - forbiddenPatterns.forEach(({ pattern, name }) => { - if (pattern.test(line)) { - violations.push({ - file: path.relative(process.cwd(), fullPath), - line: index + 1, - import: line.trim(), - module: name, - }); - } - }); - }); - } - }); - } - - scanDirectory(rendererPath); - - // Then: No Node.js modules should be imported in renderer - expect( - violations.length, - `Found Node.js module imports in renderer source code:\n\n${ - violations.map(v => `${v.file}:${v.line}\n Module: ${v.module}\n Code: ${v.import}`).join('\n\n') - }\n\nRenderer code must use the preload script or IPC to access Node.js APIs.` - ).toBe(0); - }); -}); \ No newline at end of file diff --git a/tests/smoke/electron-init.smoke.test.ts b/tests/smoke/electron-init.smoke.test.ts deleted file mode 100644 index e13a016fa..000000000 --- a/tests/smoke/electron-init.smoke.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { DIContainer } from '@apps/companion/main/di-container'; -import { StartAutomationSessionUseCase } from 'apps/companion/main/automation/application/use-cases/StartAutomationSessionUseCase'; -import { CheckAuthenticationUseCase } from 'apps/companion/main/automation/application/use-cases/CheckAuthenticationUseCase'; -import { InitiateLoginUseCase } from 'apps/companion/main/automation/application/use-cases/InitiateLoginUseCase'; -import { ClearSessionUseCase } from 'apps/companion/main/automation/application/use-cases/ClearSessionUseCase'; -import { ConfirmCheckoutUseCase } from 'apps/companion/main/automation/application/use-cases/ConfirmCheckoutUseCase'; -import { PlaywrightAutomationAdapter } from 'core/automation/infrastructure//automation'; -import { InMemorySessionRepository } from 'apps/companion/main/automation/infrastructure/repositories/InMemorySessionRepository'; -import { NoOpLogAdapter } from '@core/automation/infrastructure//logging/NoOpLogAdapter'; - -// Mock Electron's app module -vi.mock('electron', () => ({ - app: { - getPath: vi.fn((name: string) => { - if (name === 'userData') return '/tmp/test-user-data'; - return '/tmp/test'; - }), - getAppPath: vi.fn(() => '/tmp/test-app'), - isPackaged: false, - }, -})); - -describe('Electron DIContainer Smoke Tests', () => { - beforeEach(() => { - (DIContainer as unknown as { instance?: unknown }).instance = undefined; - }); - - it('DIContainer initializes without errors', () => { - expect(() => DIContainer.getInstance()).not.toThrow(); - }); - - it('All use cases are accessible', () => { - const container = DIContainer.getInstance(); - - expect(() => container.getStartAutomationUseCase()).not.toThrow(); - expect(() => container.getCheckAuthenticationUseCase()).not.toThrow(); - expect(() => container.getInitiateLoginUseCase()).not.toThrow(); - expect(() => container.getClearSessionUseCase()).not.toThrow(); - expect(() => container.getConfirmCheckoutUseCase()).not.toThrow(); - }); - - it('Use case instances are available after initialization', () => { - const container = DIContainer.getInstance(); - - // Verify all core use cases are available - expect(container.getStartAutomationUseCase()).not.toBeNull(); - expect(container.getStartAutomationUseCase()).toBeDefined(); - - // These may be null in test mode, but should not throw - expect(() => container.getCheckAuthenticationUseCase()).not.toThrow(); - expect(() => container.getInitiateLoginUseCase()).not.toThrow(); - expect(() => container.getClearSessionUseCase()).not.toThrow(); - }); - - it('Container provides access to dependencies', () => { - const container = DIContainer.getInstance(); - - // Verify core dependencies are accessible - expect(container.getSessionRepository()).toBeDefined(); - expect(container.getAutomationEngine()).toBeDefined(); - expect(container.getBrowserAutomation()).toBeDefined(); - expect(container.getLogger()).toBeDefined(); - }); - - it('ConfirmCheckoutUseCase can be verified without errors', () => { - const container = DIContainer.getInstance(); - - // This getter should not throw even if null (verifies the import) - expect(() => container.getConfirmCheckoutUseCase()).not.toThrow(); - }); -}); \ No newline at end of file diff --git a/tests/smoke/helpers/companion-boot-harness.ts b/tests/smoke/helpers/companion-boot-harness.ts deleted file mode 100644 index 3d544437c..000000000 --- a/tests/smoke/helpers/companion-boot-harness.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Experimental Playwright+Electron companion boot harness (retired). - * - * This harness attempted to launch the Electron-based companion app via - * Playwright's Electron driver, but it cannot run in this environment because - * Electron embeds Node.js 16.17.1 while the installed Playwright version - * requires Node.js 18 or higher. - * - * Companion behavior is instead covered by: - * - Playwright-based automation E2Es and integrations against fixtures. - * - Electron build/init/DI smoke tests. - * - Domain and application unit/integration tests. - * - * This file is intentionally implementation-empty to avoid misleading - * Playwright+Electron coverage while keeping the historical entrypoint - * discoverable. - */ \ No newline at end of file diff --git a/tests/smoke/helpers/console-monitor.ts b/tests/smoke/helpers/console-monitor.ts deleted file mode 100644 index 499c8aa58..000000000 --- a/tests/smoke/helpers/console-monitor.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { Page, ConsoleMessage } from '@playwright/test'; - -export interface ConsoleError { - type: 'error' | 'warning' | 'pageerror'; - message: string; - location?: string; - timestamp: Date; -} - -/** - * ConsoleMonitor - Aggregates and tracks all console output - * - * Purpose: Catch ANY runtime errors during Electron app lifecycle - * - * Critical Detections: - * - "Module has been externalized for browser compatibility" - * - "__dirname is not defined" - * - "require is not defined" - * - Any uncaught exceptions - */ -export class ConsoleMonitor { - private errors: ConsoleError[] = []; - private warnings: ConsoleError[] = []; - private isMonitoring = false; - - /** - * Start monitoring console output on the page - */ - startMonitoring(page: Page): void { - if (this.isMonitoring) { - return; - } - - // Monitor console.error calls - page.on('console', (msg: ConsoleMessage) => { - if (msg.type() === 'error') { - this.errors.push({ - type: 'error', - message: msg.text(), - location: msg.location()?.url, - timestamp: new Date(), - }); - } else if (msg.type() === 'warning') { - this.warnings.push({ - type: 'warning', - message: msg.text(), - location: msg.location()?.url, - timestamp: new Date(), - }); - } - }); - - // Monitor uncaught exceptions - page.on('pageerror', (error: Error) => { - const errorObj: ConsoleError = { - type: 'pageerror', - message: error.message, - timestamp: new Date(), - }; - if (error.stack) { - errorObj.location = error.stack; - } - this.errors.push(errorObj); - }); - - this.isMonitoring = true; - } - - /** - * Check if any errors were detected - */ - hasErrors(): boolean { - return this.errors.length > 0; - } - - /** - * Get all detected errors - */ - getErrors(): ConsoleError[] { - return [...this.errors]; - } - - /** - * Get all detected warnings - */ - getWarnings(): ConsoleError[] { - return [...this.warnings]; - } - - /** - * Format errors for test output - */ - formatErrors(): string { - if (this.errors.length === 0) { - return 'No errors detected'; - } - - const lines = ['Console errors detected during test:', '']; - - this.errors.forEach((error, index) => { - lines.push(`${index + 1}. [${error.type}] ${error.message}`); - if (error.location) { - lines.push(` Location: ${error.location}`); - } - lines.push(''); - }); - - return lines.join('\n'); - } - - /** - * Check for specific browser context errors - */ - hasBrowserContextErrors(): boolean { - const contextErrorPatterns = [ - /has been externalized for browser compatibility/i, - /__dirname is not defined/i, - /require is not defined/i, - /Cannot access .* in client code/i, - ]; - - return this.errors.some(error => - contextErrorPatterns.some(pattern => pattern.test(error.message)) - ); - } - - /** - * Reset monitoring state - */ - reset(): void { - this.errors = []; - this.warnings = []; - } -} \ No newline at end of file diff --git a/tests/smoke/helpers/electron-test-harness.ts b/tests/smoke/helpers/electron-test-harness.ts deleted file mode 100644 index ac23a41d5..000000000 --- a/tests/smoke/helpers/electron-test-harness.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { _electron as electron, ElectronApplication, Page } from '@playwright/test'; -import * as path from 'path'; - -/** - * ElectronTestHarness - Manages Electron app lifecycle for smoke tests - * - * Responsibilities: - * - Launch actual compiled Electron app - * - Wait for renderer window to open - * - Provide access to main process and renderer page - * - Clean shutdown - */ -export class ElectronTestHarness { - private app: ElectronApplication | null = null; - private mainWindow: Page | null = null; - - /** - * Launch Electron app and wait for main window - * - * @throws Error if app fails to launch or window doesn't open - */ - async launch(): Promise { - // Path to the built Electron app entry point - const electronEntryPath = path.join(__dirname, '../../../apps/companion/dist/main/main.cjs'); - - // Launch Electron app with the compiled entry file - // Note: Playwright may have compatibility issues with certain Electron versions - // regarding --remote-debugging-port flag - const launchOptions: any = { - args: [electronEntryPath], - env: { - ...Object.fromEntries(Object.entries(process.env).filter(([_, v]) => v !== undefined)), - NODE_ENV: 'test', - }, - }; - if (process.env.ELECTRON_EXECUTABLE_PATH) { - launchOptions.executablePath = process.env.ELECTRON_EXECUTABLE_PATH; - } - this.app = await electron.launch(launchOptions); - - // Wait for first window (renderer process) - this.mainWindow = await this.app.firstWindow({ - timeout: 10_000, - }); - - // Wait for React to render - await this.mainWindow.waitForLoadState('domcontentloaded'); - } - - /** - * Get the main renderer window - */ - getMainWindow(): Page { - if (!this.mainWindow) { - throw new Error('Main window not available. Did you call launch()?'); - } - return this.mainWindow; - } - - /** - * Get the Electron app instance for IPC testing - */ - getApp(): ElectronApplication { - if (!this.app) { - throw new Error('Electron app not available. Did you call launch()?'); - } - return this.app; - } - - /** - * Clean shutdown of Electron app - */ - async close(): Promise { - if (this.app) { - await this.app.close(); - this.app = null; - this.mainWindow = null; - } - } -} \ No newline at end of file diff --git a/tests/smoke/helpers/ipc-verifier.ts b/tests/smoke/helpers/ipc-verifier.ts deleted file mode 100644 index 13d121592..000000000 --- a/tests/smoke/helpers/ipc-verifier.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { ElectronApplication } from '@playwright/test'; - -type IpcHandlerResult = { - error?: string; - [key: string]: unknown; -}; - -export interface IPCTestResult { - channel: string; - success: boolean; - error?: string; - duration: number; -} - -/** - * IPCVerifier - Tests IPC channel contracts - * - * Purpose: Verify main <-> renderer communication works - * Scope: Core IPC channels required for app functionality - */ -export class IPCVerifier { - constructor(private app: ElectronApplication) {} - - /** - * Test checkAuth IPC channel - */ - async testCheckAuth(): Promise { - const start = Date.now(); - const channel = 'auth:check'; - - try { - const result = await this.app.evaluate( - async ({ ipcMain }: { ipcMain: { listeners: (channel: string) => unknown[] } }) => { - return new Promise((resolve) => { - // Simulate IPC invoke handler by calling the first registered handler for the channel - const handlers = ipcMain.listeners('auth:check') || []; - const handler = handlers[0] as - | ((event: unknown, ...args: unknown[]) => unknown | Promise) - | undefined; - - if (!handler) { - resolve({ error: 'Handler not registered' }); - } else { - // Invoke the handler similar to ipcMain.handle invocation signature - // (event, ...args) => Promise - const mockEvent: unknown = {}; - Promise.resolve(handler(mockEvent)) - .then((res: unknown) => resolve(res)) - .catch((err: unknown) => - resolve({ - error: - err && err instanceof Error && err.message - ? err.message - : String(err), - }), - ); - } - }); - }, - ); - - const typed: IpcHandlerResult = result as IpcHandlerResult; - - const resultObj: IPCTestResult = { - channel, - success: !typed.error, - duration: Date.now() - start, - }; - if (typed.error) { - resultObj.error = typed.error; - } - return resultObj; - } catch (error) { - return { - channel, - success: false, - error: error instanceof Error ? error.message : String(error), - duration: Date.now() - start, - }; - } - } - - /** - * Test getBrowserMode IPC channel - */ - async testGetBrowserMode(): Promise { - const start = Date.now(); - const channel = 'browser-mode:get'; - - try { - const result = await this.app.evaluate( - async ({ ipcMain }: { ipcMain: { listeners: (channel: string) => unknown[] } }) => { - return new Promise((resolve) => { - const handlers = ipcMain.listeners('browser-mode:get') || []; - const handler = handlers[0] as - | ((event: unknown, ...args: unknown[]) => unknown | Promise) - | undefined; - - if (!handler) { - resolve({ error: 'Handler not registered' }); - } else { - const mockEvent: unknown = {}; - Promise.resolve(handler(mockEvent)) - .then((res: unknown) => resolve(res)) - .catch((err: unknown) => - resolve({ - error: - err && err instanceof Error && err.message - ? err.message - : String(err), - }), - ); - } - }); - }, - ); - - const typed: IpcHandlerResult = result as IpcHandlerResult; - - const resultObj: IPCTestResult = { - channel, - success: !typed.error, - duration: Date.now() - start, - }; - if (typed.error) { - resultObj.error = typed.error; - } - return resultObj; - } catch (error) { - return { - channel, - success: false, - error: error instanceof Error ? error.message : String(error), - duration: Date.now() - start, - }; - } - } - - /** - * Test startAutomationSession IPC channel contract - */ - async testStartAutomationSession(): Promise { - const start = Date.now(); - const channel = 'start-automation'; - - try { - const result = await this.app.evaluate( - async ({ ipcMain }: { ipcMain: { listeners: (channel: string) => unknown[] } }) => { - return new Promise((resolve) => { - const handlers = ipcMain.listeners('start-automation') || []; - const handler = handlers[0] as - | (( - event: unknown, - payload: { sessionName: string; mode: string }, - ) => unknown | Promise) - | undefined; - - if (!handler) { - resolve({ error: 'Handler not registered' }); - } else { - // Test with mock data - const mockEvent: unknown = {}; - Promise.resolve( - handler(mockEvent, { sessionName: 'test', mode: 'test' }), - ) - .then((res: unknown) => resolve(res)) - .catch((err: unknown) => - resolve({ - error: - err && err instanceof Error && err.message - ? err.message - : String(err), - }), - ); - } - }); - }, - ); - - const typed: IpcHandlerResult = result as IpcHandlerResult; - - const resultObj: IPCTestResult = { - channel, - success: !typed.error, - duration: Date.now() - start, - }; - if (typed.error) { - resultObj.error = typed.error; - } - return resultObj; - } catch (error) { - return { - channel, - success: false, - error: error instanceof Error ? error.message : String(error), - duration: Date.now() - start, - }; - } - } - - /** - * Run all IPC tests and return results - */ - async verifyAllChannels(): Promise { - return Promise.all([ - this.testCheckAuth(), - this.testGetBrowserMode(), - this.testStartAutomationSession(), - ]); - } - - /** - * Format IPC test results for output - */ - static formatResults(results: IPCTestResult[]): string { - const lines = ['IPC Channel Verification:', '']; - - results.forEach(result => { - const status = result.success ? '✓' : '✗'; - lines.push(`${status} ${result.channel} (${result.duration}ms)`); - if (result.error) { - lines.push(` Error: ${result.error}`); - } - }); - - return lines.join('\n'); - } -} \ No newline at end of file diff --git a/tests/smoke/playwright-init.smoke.test.ts b/tests/smoke/playwright-init.smoke.test.ts deleted file mode 100644 index 1999b5e39..000000000 --- a/tests/smoke/playwright-init.smoke.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { describe, it, expect, afterEach, beforeAll, afterAll } from 'vitest'; -import { PlaywrightAutomationAdapter, FixtureServer } from 'core/automation/infrastructure//automation'; -import { NoOpLogAdapter } from '@core/automation/infrastructure//logging/NoOpLogAdapter'; - -describe('Playwright Adapter Smoke Tests', () => { - let adapter: PlaywrightAutomationAdapter | undefined; - let server: FixtureServer | undefined; - let unhandledRejectionHandler: ((reason: unknown) => void) | null = null; - const logger = new NoOpLogAdapter(); - - beforeAll(() => { - unhandledRejectionHandler = (reason: unknown) => { - const message = - reason instanceof Error ? reason.message : String(reason ?? ''); - if (message.includes('cdpSession.send: Target page, context or browser has been closed')) { - return; - } - throw reason; - }; - (process as any).on('unhandledRejection', unhandledRejectionHandler); - }); - - afterEach(async () => { - if (adapter) { - try { - await adapter.disconnect(); - } catch { - // Ignore cleanup errors - } - adapter = undefined; - } - if (server) { - try { - await server.stop(); - } catch { - // Ignore cleanup errors - } - server = undefined; - } - }); - - afterAll(() => { - if (unhandledRejectionHandler) { - (process as any).removeListener('unhandledRejection', unhandledRejectionHandler); - unhandledRejectionHandler = null; - } - }); - - it('Adapter instantiates without errors', () => { - expect(() => { - adapter = new PlaywrightAutomationAdapter({ - headless: true, - mode: 'mock', - timeout: 5000, - }, logger); - }).not.toThrow(); - }); - - it('Browser connects successfully', async () => { - adapter = new PlaywrightAutomationAdapter({ - headless: true, - mode: 'mock', - timeout: 5000, - }, logger); - - const result = await adapter.connect(); - expect(result.success).toBe(true); - expect(adapter.isConnected()).toBe(true); - }); - - it('Basic navigation works with mock fixtures', async () => { - server = new FixtureServer(); - await server.start(); - - adapter = new PlaywrightAutomationAdapter({ - headless: true, - mode: 'mock', - timeout: 5000, - }, logger); - - await adapter.connect(); - const navResult = await adapter.navigateToPage(server.getFixtureUrl(2)); - expect(navResult.success).toBe(true); - }); - - it('Adapter can be instantiated multiple times', () => { - expect(() => { - const adapter1 = new PlaywrightAutomationAdapter({ - headless: true, - mode: 'mock', - timeout: 5000, - }, logger); - const adapter2 = new PlaywrightAutomationAdapter({ - headless: true, - mode: 'mock', - timeout: 5000, - }, logger); - expect(adapter1).not.toBe(adapter2); - }).not.toThrow(); - }); - - it('FixtureServer starts and stops cleanly', async () => { - server = new FixtureServer(); - - await expect(server.start()).resolves.not.toThrow(); - expect(server.getFixtureUrl(2)).toContain('http://localhost:'); - await expect(server.stop()).resolves.not.toThrow(); - }); -}); \ No newline at end of file diff --git a/vitest.api.config.ts b/vitest.api.config.ts index 249cf5ef3..333341181 100644 --- a/vitest.api.config.ts +++ b/vitest.api.config.ts @@ -1,5 +1,5 @@ import { defineConfig } from 'vitest/config'; -import { resolve } from 'node:path'; +import * as path from 'path'; export default defineConfig({ test: { @@ -35,9 +35,9 @@ export default defineConfig({ }, resolve: { alias: { - '@core': resolve(__dirname, './core'), - '@adapters': resolve(__dirname, './adapters'), - '@testing': resolve(__dirname, './testing'), + '@core': path.resolve(__dirname, './core'), + '@adapters': path.resolve(__dirname, './adapters'), + '@testing': path.resolve(__dirname, './testing'), }, }, }); \ No newline at end of file diff --git a/vitest.config.ts b/vitest.config.ts index 32ac1caab..4d5ae99e9 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -15,13 +15,13 @@ export default defineConfig({ 'core/**/*.{test,spec}.?(c|m)[jt]s?(x)', 'adapters/**/*.{test,spec}.?(c|m)[jt]s?(x)', 'apps/**/*.{test,spec}.?(c|m)[jt]s?(x)', + 'tests/integration/**/*.{test,spec}.?(c|m)[jt]s?(x)', ], exclude: [ 'node_modules/**', '**/dist/**', '**/.next/**', 'tests/smoke/website-pages.spec.ts', - 'apps/companion/**', ], }, resolve: { diff --git a/vitest.e2e.config.ts b/vitest.e2e.config.ts index 7799a68e9..12b0003ed 100644 --- a/vitest.e2e.config.ts +++ b/vitest.e2e.config.ts @@ -1,5 +1,5 @@ import { defineConfig } from 'vitest/config'; -import path from 'path'; +import * as path from 'path'; /** * E2E Test Configuration @@ -12,6 +12,11 @@ export default defineConfig({ globals: true, environment: 'node', include: ['tests/e2e/**/*.e2e.test.ts'], + exclude: [ + '**/companion/**', + '**/*companion*.e2e.test.ts', + 'tests/e2e/companion/**', + ], // E2E tests use real automation - set strict timeouts to prevent hanging // Individual tests: 30 seconds max testTimeout: 30000, diff --git a/vitest.smoke.config.ts b/vitest.smoke.config.ts index 19d925df8..aaaa15505 100644 --- a/vitest.smoke.config.ts +++ b/vitest.smoke.config.ts @@ -1,13 +1,20 @@ import { defineConfig } from 'vitest/config'; -import path from 'path'; +import * as path from 'path'; export default defineConfig({ test: { globals: true, environment: 'node', include: [ + // Companion-related smoke tests are excluded + ], + exclude: [ + '**/companion/**', + '**/*companion*.test.ts', 'tests/smoke/electron-init.smoke.test.ts', 'tests/smoke/browser-mode-toggle.smoke.test.ts', + 'tests/smoke/di-container.test.ts', + 'tests/smoke/electron-build.smoke.test.ts', ], testTimeout: 10000, hookTimeout: 10000,