Files
gridpilot.gg/docs/MOCK_FIXTURES_DESIGN.md

835 lines
22 KiB
Markdown

# Mock HTML Fixtures Design Document
## Overview
This document specifies simplified mock HTML fixtures with explicit test attributes for browser automation testing. These fixtures replace the current full-page iRacing dumps with lightweight, testable HTML pages that simulate the iRacing hosted session wizard.
## Purpose
**Test fixtures for E2E testing** - Simplified HTML pages served by FixtureServer that simulate iRacing's wizard for testing the PlaywrightAutomationAdapter in isolation, without needing access to the real iRacing website.
## Problem Statement
Current fixtures in `resources/iracing-hosted-sessions/`:
- Full page dumps (~2.4M tokens per file)
- React/Chakra UI with obfuscated CSS classes (`css-451i2c`, etc.)
- No stable `data-testid` or `data-automation` attributes
- Unsuitable for reliable CSS selector-based automation
## Solution: Simplified Mock Fixtures
### Design Principles
1. **Explicit Test Attributes**: Every interactive element has stable `data-*` attributes
2. **Minimal HTML**: Only essential structure, no framework artifacts
3. **Self-Contained**: Each fixture includes all CSS needed for visual verification
4. **Navigation-Aware**: Buttons link to appropriate next/previous fixtures
5. **Form Fields Match Domain**: Field names align with `HostedSessionConfig` entity
---
## Attribute Schema
### Core Attributes
| Attribute | Purpose | Example Values |
|-----------|---------|----------------|
| `data-step` | Step identification | `2` through `18` |
| `data-action` | Navigation/action buttons | `next`, `back`, `confirm`, `cancel`, `create`, `add`, `select` |
| `data-field` | Form input fields | `sessionName`, `password`, `description`, `region`, etc. |
| `data-modal` | Modal container flag | `true` |
| `data-modal-trigger` | Button that opens a modal | `admin`, `car`, `track` |
| `data-list` | List container | `admins`, `cars`, `tracks` |
| `data-item` | Selectable list item | Car/track/admin ID |
| `data-toggle` | Toggle/checkbox element | `startNow`, `teamDriving`, `rollingStart` |
| `data-dropdown` | Dropdown select | `region`, `weather`, `trackState`, `carClass` |
| `data-slider` | Slider input | `time`, `temperature`, `practice`, `qualify`, `race` |
| `data-indicator` | Step indicator | `race-info`, `server-details`, etc. |
### Navigation Attribute Values
| Value | Description | Usage |
|-------|-------------|-------|
| `next` | Proceed to next step | All non-final steps |
| `back` | Return to previous step | Steps 3-18 |
| `confirm` | Confirm modal action | Modal steps (6, 9, 12) |
| `cancel` | Cancel/close modal | Modal steps |
| `create` | Create new race | Step 2 |
| `add` | Open add modal | Steps 5, 8, 11 |
| `select` | Select item from list | Modal list items |
---
## Step-by-Step Fixture Specifications
### Step 1: Login - Handled Externally
> Note: Login is handled externally. No fixture needed.
### Step 2: Hosted Racing - Main Page
**Purpose**: Landing page with Create a Race button
**Elements**:
```
data-step="2"
data-indicator="hosted-racing"
data-action="create" → Button: Create a Race
```
**Fields**: None
**Navigation**:
- `[data-action="create"]` → Step 3
---
### Step 3: Race Information
**Purpose**: Basic session configuration
**Elements**:
```
data-step="3"
data-indicator="race-information"
data-field="sessionName" → Input: Session name - required
data-field="password" → Input: Session password - optional
data-field="description" → Textarea: Session description
data-action="next" → Button: Next
data-action="back" → Button: Back
```
**Fields**:
| Field | Type | Required | Domain Property |
|-------|------|----------|-----------------|
| `sessionName` | text | Yes | `config.sessionName` |
| `password` | password | No | `config.password` |
| `description` | textarea | No | `config.description` |
---
### Step 4: Server Details
**Purpose**: Server region and timing configuration
**Elements**:
```
data-step="4"
data-indicator="server-details"
data-dropdown="region" → Select: Server region
data-toggle="startNow" → Checkbox: Start immediately
data-action="next"
data-action="back"
```
**Fields**:
| Field | Type | Options |
|-------|------|---------|
| `region` | dropdown | `us-east`, `us-west`, `eu-central`, `eu-west`, `asia`, `oceania` |
| `startNow` | toggle | Boolean |
---
### Step 5: Set Admins
**Purpose**: Admin list management
**Elements**:
```
data-step="5"
data-indicator="set-admins"
data-list="admins" → Container: Admin list
data-modal-trigger="admin" → Button: Add Admin
data-action="next"
data-action="back"
```
---
### Step 6: Add an Admin - Modal
**Purpose**: Search and select admin to add
**Elements**:
```
data-step="6"
data-modal="true"
data-indicator="add-admin"
data-field="adminSearch" → Input: Search admins
data-list="adminResults" → Container: Search results
data-item="{adminId}" → Each result item
data-action="select" → Button: Select admin
data-action="confirm" → Button: Add Selected
data-action="cancel" → Button: Cancel
```
**Fields**:
| Field | Type | Purpose |
|-------|------|---------|
| `adminSearch` | text | Filter admin list |
---
### Step 7: Time Limits
**Purpose**: Practice, qualify, and race duration settings
**Elements**:
```
data-step="7"
data-indicator="time-limits"
data-slider="practice" → Range: Practice length in minutes
data-slider="qualify" → Range: Qualify length in minutes
data-slider="race" → Range: Race length in laps or minutes
data-toggle="unlimitedTime" → Checkbox: Unlimited time
data-action="next"
data-action="back"
```
**Fields**:
| Field | Type | Range | Default |
|-------|------|-------|---------|
| `practice` | slider | 0-120 min | 15 |
| `qualify` | slider | 0-60 min | 10 |
| `race` | slider | 1-500 laps | 20 |
---
### Step 8: Set Cars
**Purpose**: Car list management
**Elements**:
```
data-step="8"
data-indicator="set-cars"
data-list="cars" → Container: Selected cars
data-modal-trigger="car" → Button: Add Car
data-action="next"
data-action="back"
```
---
### Step 9: Add a Car - Modal
**Purpose**: Search and select cars
**Elements**:
```
data-step="9"
data-modal="true"
data-indicator="add-car"
data-field="carSearch" → Input: Search cars
data-list="carResults" → Container: Car grid
data-item="{carId}" → Each car tile
data-action="select" → Select car
data-action="confirm" → Button: Add Selected
data-action="cancel" → Button: Cancel
```
---
### Step 10: Set Car Classes
**Purpose**: Multi-class race configuration
**Elements**:
```
data-step="10"
data-indicator="car-classes"
data-dropdown="carClass" → Select: Car class assignment
data-list="classAssignments" → Container: Class assignments
data-action="next"
data-action="back"
```
---
### Step 11: Set Track
**Purpose**: Track selection
**Elements**:
```
data-step="11"
data-indicator="set-track"
data-field="selectedTrack" → Display: Currently selected track
data-modal-trigger="track" → Button: Select Track
data-action="next"
data-action="back"
```
---
### Step 12: Add a Track - Modal
**Purpose**: Search and select track
**Elements**:
```
data-step="12"
data-modal="true"
data-indicator="add-track"
data-field="trackSearch" → Input: Search tracks
data-list="trackResults" → Container: Track grid
data-item="{trackId}" → Each track tile
data-action="select" → Select track
data-action="confirm" → Button: Select
data-action="cancel" → Button: Cancel
```
---
### Step 13: Track Options
**Purpose**: Track configuration selection
**Elements**:
```
data-step="13"
data-indicator="track-options"
data-dropdown="trackConfig" → Select: Track configuration
data-toggle="dynamicTrack" → Checkbox: Dynamic track
data-action="next"
data-action="back"
```
---
### Step 14: Time of Day
**Purpose**: Race start time configuration
**Elements**:
```
data-step="14"
data-indicator="time-of-day"
data-slider="timeOfDay" → Range: Time of day 0-24
data-field="raceDate" → Date picker: Race date
data-toggle="simulatedTime" → Checkbox: Simulated time progression
data-action="next"
data-action="back"
```
---
### Step 15: Weather
**Purpose**: Weather conditions
**Elements**:
```
data-step="15"
data-indicator="weather"
data-dropdown="weatherType" → Select: Weather type
data-slider="temperature" → Range: Temperature
data-slider="humidity" → Range: Humidity
data-toggle="dynamicWeather" → Checkbox: Dynamic weather
data-action="next"
data-action="back"
```
**Weather Types**: `clear`, `partly-cloudy`, `mostly-cloudy`, `overcast`
---
### Step 16: Race Options
**Purpose**: Race rules and settings
**Elements**:
```
data-step="16"
data-indicator="race-options"
data-field="maxDrivers" → Input: Maximum drivers
data-toggle="rollingStart" → Checkbox: Rolling start
data-toggle="fullCourseCautions" → Checkbox: Full course cautions
data-toggle="fastRepairs" → Checkbox: Fast repairs
data-action="next"
data-action="back"
```
---
### Step 17: Team Driving
**Purpose**: Team race configuration
**Elements**:
```
data-step="17"
data-indicator="team-driving"
data-toggle="teamDriving" → Checkbox: Enable team driving
data-field="minDrivers" → Input: Min drivers per team
data-field="maxDrivers" → Input: Max drivers per team
data-action="next"
data-action="back"
```
---
### Step 18: Track Conditions - Final Step
**Purpose**: Track state configuration
**Elements**:
```
data-step="18"
data-indicator="track-conditions"
data-dropdown="trackState" → Select: Track state
data-toggle="marbles" → Checkbox: Marbles simulation
data-slider="rubberLevel" → Range: Rubber buildup
data-action="back" → Button: Back
```
**Track States**: `auto-generated`, `clean`, `low-rubber`, `medium-rubber`, `high-rubber`
> **Note**: No Submit button on Step 18. Automation intentionally stops here for user review.
---
## Navigation Flow Diagram
```mermaid
flowchart TD
S2[Step 2: Hosted Racing] -->|Create Race| S3[Step 3: Race Information]
S3 -->|Next| S4[Step 4: Server Details]
S4 -->|Next| S5[Step 5: Set Admins]
S5 -->|Add Admin| S6[Step 6: Add Admin Modal]
S6 -->|Confirm/Cancel| S5
S5 -->|Next| S7[Step 7: Time Limits]
S7 -->|Next| S8[Step 8: Set Cars]
S8 -->|Add Car| S9[Step 9: Add Car Modal]
S9 -->|Confirm/Cancel| S8
S8 -->|Next| S10[Step 10: Car Classes]
S10 -->|Next| S11[Step 11: Set Track]
S11 -->|Select Track| S12[Step 12: Add Track Modal]
S12 -->|Confirm/Cancel| S11
S11 -->|Next| S13[Step 13: Track Options]
S13 -->|Next| S14[Step 14: Time of Day]
S14 -->|Next| S15[Step 15: Weather]
S15 -->|Next| S16[Step 16: Race Options]
S16 -->|Next| S17[Step 17: Team Driving]
S17 -->|Next| S18[Step 18: Track Conditions]
S18 -->|STOP| REVIEW[Manual Review Required]
S3 -.->|Back| S2
S4 -.->|Back| S3
S5 -.->|Back| S4
S7 -.->|Back| S5
S8 -.->|Back| S7
S10 -.->|Back| S8
S11 -.->|Back| S10
S13 -.->|Back| S11
S14 -.->|Back| S13
S15 -.->|Back| S14
S16 -.->|Back| S15
S17 -.->|Back| S16
S18 -.->|Back| S17
```
---
## Example Fixture: Step 3 - Race Information
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iRacing - Race Information</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif;
background: #1a1a2e;
color: #eee;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: #16213e;
padding: 16px 24px;
border-bottom: 1px solid #0f3460;
}
.step-indicator {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #888;
}
.step-indicator .current {
color: #e94560;
font-weight: bold;
}
.main {
flex: 1;
padding: 32px 24px;
max-width: 600px;
margin: 0 auto;
width: 100%;
}
.page-title {
font-size: 24px;
font-weight: 600;
margin-bottom: 24px;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
font-size: 14px;
color: #aaa;
margin-bottom: 6px;
}
.form-label.required::after {
content: " *";
color: #e94560;
}
.form-input {
width: 100%;
padding: 12px 16px;
background: #16213e;
border: 1px solid #0f3460;
border-radius: 4px;
color: #eee;
font-size: 16px;
}
.form-input:focus {
outline: none;
border-color: #e94560;
}
textarea.form-input {
min-height: 100px;
resize: vertical;
}
.footer {
background: #16213e;
padding: 16px 24px;
border-top: 1px solid #0f3460;
display: flex;
justify-content: space-between;
}
.btn {
padding: 12px 24px;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
border: none;
transition: all 0.2s;
}
.btn-primary {
background: #e94560;
color: white;
}
.btn-primary:hover {
background: #ff6b6b;
}
.btn-secondary {
background: transparent;
color: #aaa;
border: 1px solid #0f3460;
}
.btn-secondary:hover {
background: #0f3460;
color: #eee;
}
</style>
</head>
<body data-step="3">
<header class="header">
<div class="step-indicator" data-indicator="race-information">
<span>Step</span>
<span class="current">3</span>
<span>of 18</span>
<span></span>
<span>Race Information</span>
</div>
</header>
<main class="main">
<h1 class="page-title">Race Information</h1>
<form id="race-info-form">
<div class="form-group">
<label class="form-label required" for="sessionName">Session Name</label>
<input
type="text"
id="sessionName"
class="form-input"
data-field="sessionName"
placeholder="Enter session name"
required
/>
</div>
<div class="form-group">
<label class="form-label" for="password">Password</label>
<input
type="password"
id="password"
class="form-input"
data-field="password"
placeholder="Optional password"
/>
</div>
<div class="form-group">
<label class="form-label" for="description">Description</label>
<textarea
id="description"
class="form-input"
data-field="description"
placeholder="Optional session description"
></textarea>
</div>
</form>
</main>
<footer class="footer">
<button
type="button"
class="btn btn-secondary"
data-action="back"
onclick="window.location.href='step-02-hosted-racing.html'"
>
Back
</button>
<button
type="button"
class="btn btn-primary"
data-action="next"
onclick="window.location.href='step-04-server-details.html'"
>
Next
</button>
</footer>
</body>
</html>
```
---
## Selector Strategy for PlaywrightAutomationAdapter
### Primary Selector Pattern
Use **data-* attribute selectors** as the primary strategy:
```typescript
// Selector constants
const SELECTORS = {
// Step identification
stepContainer: (step: number) => `[data-step="${step}"]`,
stepIndicator: (name: string) => `[data-indicator="${name}"]`,
// Navigation
nextButton: '[data-action="next"]',
backButton: '[data-action="back"]',
confirmButton: '[data-action="confirm"]',
cancelButton: '[data-action="cancel"]',
createButton: '[data-action="create"]',
addButton: '[data-action="add"]',
selectButton: '[data-action="select"]',
// Form fields
field: (name: string) => `[data-field="${name}"]`,
dropdown: (name: string) => `[data-dropdown="${name}"]`,
toggle: (name: string) => `[data-toggle="${name}"]`,
slider: (name: string) => `[data-slider="${name}"]`,
// Modals
modal: '[data-modal="true"]',
modalTrigger: (type: string) => `[data-modal-trigger="${type}"]`,
// Lists and items
list: (name: string) => `[data-list="${name}"]`,
listItem: (id: string) => `[data-item="${id}"]`,
};
```
### PlaywrightAutomationAdapter Integration
```typescript
import { Page } from 'playwright';
export class PlaywrightAutomationAdapter implements IScreenAutomation {
private page: Page;
async waitForStep(stepNumber: number): Promise<void> {
await this.page.waitForSelector(`[data-step="${stepNumber}"]`, {
state: 'visible',
timeout: 10000,
});
}
async clickAction(action: string): Promise<ClickResult> {
const selector = `[data-action="${action}"]`;
await this.page.click(selector);
return { success: true };
}
async fillField(fieldName: string, value: string): Promise<FormFillResult> {
const selector = `[data-field="${fieldName}"]`;
await this.page.fill(selector, value);
return { success: true, fieldName, value };
}
async selectDropdown(name: string, value: string): Promise<void> {
const selector = `[data-dropdown="${name}"]`;
await this.page.selectOption(selector, value);
}
async setToggle(name: string, checked: boolean): Promise<void> {
const selector = `[data-toggle="${name}"]`;
const isChecked = await this.page.isChecked(selector);
if (isChecked !== checked) {
await this.page.click(selector);
}
}
async setSlider(name: string, value: number): Promise<void> {
const selector = `[data-slider="${name}"]`;
await this.page.fill(selector, String(value));
}
async waitForModal(): Promise<void> {
await this.page.waitForSelector('[data-modal="true"]', {
state: 'visible',
});
}
async selectListItem(itemId: string): Promise<void> {
const selector = `[data-item="${itemId}"]`;
await this.page.click(selector);
}
async executeStep(stepId: StepId, config: SessionConfig): Promise<AutomationResult> {
const step = stepId.value;
await this.waitForStep(step);
switch (step) {
case 2:
await this.clickAction('create');
break;
case 3:
await this.fillField('sessionName', config.sessionName);
if (config.password) {
await this.fillField('password', config.password);
}
if (config.description) {
await this.fillField('description', config.description);
}
await this.clickAction('next');
break;
// Additional steps follow same pattern...
}
return { success: true };
}
}
```
### Selector Priority Order
1. **`data-action`** - For all clickable navigation elements
2. **`data-field`** - For all form inputs
3. **`data-step`** - For step identification/verification
4. **`data-modal`** - For modal detection
5. **`data-item`** - For list item selection
### Benefits of This Strategy
1. **Stability**: Selectors will not break when CSS/styling changes
2. **Clarity**: Self-documenting selectors indicate purpose
3. **Consistency**: Same pattern across all steps
4. **Testability**: Easy to verify correct element targeting
5. **Maintenance**: Simple to update when workflow changes
---
## File Structure
```
resources/
└── mock-fixtures/ # NEW: Simplified test fixtures
├── step-02-hosted-racing.html
├── step-03-race-information.html
├── step-04-server-details.html
├── step-05-set-admins.html
├── step-06-add-admin.html
├── step-07-time-limits.html
├── step-08-set-cars.html
├── step-09-add-car.html
├── step-10-car-classes.html
├── step-11-set-track.html
├── step-12-add-track.html
├── step-13-track-options.html
├── step-14-time-of-day.html
├── step-15-weather.html
├── step-16-race-options.html
├── step-17-team-driving.html
├── step-18-track-conditions.html
└── shared.css # Optional: Shared styles
```
---
## FixtureServer Updates
Update `STEP_TO_FIXTURE` mapping in [`FixtureServer.ts`](../packages/infrastructure/adapters/automation/FixtureServer.ts:16):
```typescript
const STEP_TO_FIXTURE: Record<number, string> = {
2: 'step-02-hosted-racing.html',
3: 'step-03-race-information.html',
4: 'step-04-server-details.html',
5: 'step-05-set-admins.html',
6: 'step-06-add-admin.html',
7: 'step-07-time-limits.html',
8: 'step-08-set-cars.html',
9: 'step-09-add-car.html',
10: 'step-10-car-classes.html',
11: 'step-11-set-track.html',
12: 'step-12-add-track.html',
13: 'step-13-track-options.html',
14: 'step-14-time-of-day.html',
15: 'step-15-weather.html',
16: 'step-16-race-options.html',
17: 'step-17-team-driving.html',
18: 'step-18-track-conditions.html',
};
```
---
## Implementation Tasks for Code Mode
1. Create `resources/mock-fixtures/` directory
2. Create 17 HTML fixture files for steps 2-18
3. Update [`FixtureServer.ts`](../packages/infrastructure/adapters/automation/FixtureServer.ts:42) constructor to use new fixtures path
4. Create `PlaywrightAutomationAdapter` implementing selector strategy
5. Update E2E tests to use PlaywrightAutomationAdapter with FixtureServer
---
## Testing Verification Checklist
For each fixture, verify:
- [ ] `data-step` attribute present on body
- [ ] `data-indicator` present for step identification
- [ ] All navigation buttons have `data-action`
- [ ] All form fields have `data-field`, `data-dropdown`, `data-toggle`, or `data-slider`
- [ ] Modal fixtures have `data-modal="true"`
- [ ] Navigation links point to correct next/previous fixtures
- [ ] Visual rendering is acceptable in browser