Files
PHANTOM/e2e/lib/event-generator.ts
Claude c8ac2cb609 Add dynamic pricing E2E test suite with Playwright
Implement comprehensive E2E tests to validate the surge pricing pipeline:
- Test SimpleSurgePricer with configurable thresholds (high=3, surge=1.5x)
- Verify discount pricing when demand is below low_threshold
- Test multi-product differential pricing based on demand signals
- Validate price propagation from pipeline through Redis to API

Test infrastructure:
- Playwright configuration with custom fixtures
- Python pipeline worker for direct test execution (bypasses Airflow)
- API client for event ingestion and price verification
- Event generator for creating realistic interaction sequences
- docker-compose.e2e.yml with minimal services for testing
2025-12-26 09:35:07 +00:00

250 lines
6.0 KiB
TypeScript

import { EventPayload, PriceLogPayload } from './api-client';
import { v4 as uuidv4 } from 'uuid';
/**
* Canonical event names matching the frontend
*/
export const EventNames = {
// Navigation events
PAGE_VIEW: 'page_view',
VIEW_ITEM_PAGE: 'view_item_page',
LEARN_MORE: 'learn_more_about_item',
// Cart events
ADD_TO_CART: 'add_item_to_cart',
REMOVE_FROM_CART: 'remove_item',
CHECKOUT_START: 'checkout_start',
PURCHASE_COMPLETE: 'purchase_complete',
// Search/Filter events
SEARCH: 'search',
FILTER_DATE: 'filter_for_date',
FILTER_AMENITIES: 'filter_for_amenities',
FILTER_PRICE: 'filter_for_price',
SORT_CHANGE: 'sort_change',
// Dwell signals (engagement)
HOVER_TITLE: 'hover_over_title',
HOVER_PARAGRAPH: 'hover_over_paragraph',
HOVER_LINK: 'hover_over_link',
HOVER_BUTTON: 'hover_over_button',
// Session
SESSION_START: 'session_start',
} as const;
export type EventName = typeof EventNames[keyof typeof EventNames];
/**
* Test product configuration
*/
export interface TestProduct {
id: string;
basePrice: number;
storeMode: 'hotel' | 'airline';
name?: string;
}
/**
* Generates test events for dynamic pricing E2E tests
*/
export class EventGenerator {
private sessionId: string;
private experimentId: string;
private storeMode: 'hotel' | 'airline';
constructor(options?: {
sessionId?: string;
experimentId?: string;
storeMode?: 'hotel' | 'airline';
}) {
this.sessionId = options?.sessionId || uuidv4();
this.experimentId = options?.experimentId || uuidv4();
this.storeMode = options?.storeMode || 'hotel';
}
get session(): string {
return this.sessionId;
}
get experiment(): string {
return this.experimentId;
}
/**
* Create a new session for isolation between test scenarios
*/
newSession(): string {
this.sessionId = uuidv4();
return this.sessionId;
}
/**
* Generate a single event
*/
createEvent(
eventName: EventName,
productId: string,
metadata?: Record<string, unknown>
): EventPayload {
return {
sessionId: this.sessionId,
experimentId: this.experimentId,
eventName,
page: `/${this.storeMode}/products/${productId}`,
productId,
metadata: metadata || {},
storeMode: this.storeMode,
userAgent: 'PHANTOM-E2E-Test/1.0',
ts: new Date().toISOString(),
};
}
/**
* Generate a product view event
*/
viewProduct(productId: string): EventPayload {
return this.createEvent(EventNames.VIEW_ITEM_PAGE, productId, {
referrer: `/${this.storeMode}/products`,
viewport: { width: 1920, height: 1080 },
});
}
/**
* Generate a "learn more" event (high intent signal)
*/
learnMore(productId: string): EventPayload {
return this.createEvent(EventNames.LEARN_MORE, productId, {
section: 'details',
});
}
/**
* Generate a hover event (engagement signal)
*/
hover(productId: string, element: 'title' | 'paragraph' | 'button' = 'title'): EventPayload {
const eventMap = {
title: EventNames.HOVER_TITLE,
paragraph: EventNames.HOVER_PARAGRAPH,
button: EventNames.HOVER_BUTTON,
};
return this.createEvent(eventMap[element], productId, {
duration_ms: Math.floor(Math.random() * 2000) + 500,
});
}
/**
* Generate an add-to-cart event
*/
addToCart(productId: string, quantity: number = 1): EventPayload {
return this.createEvent(EventNames.ADD_TO_CART, productId, {
quantity,
cart_size: quantity,
});
}
/**
* Generate a sequence of high-velocity events for surge pricing trigger
* This simulates rapid user interest in a product
*/
generateSurgeSequence(productId: string, count: number): EventPayload[] {
const events: EventPayload[] = [];
for (let i = 0; i < count; i++) {
// Mix of different event types to simulate realistic behavior
events.push(this.viewProduct(productId));
if (i % 2 === 0) {
events.push(this.learnMore(productId));
}
if (i % 3 === 0) {
events.push(this.hover(productId, 'title'));
}
}
return events;
}
/**
* Generate a normal browsing session (not triggering surge)
*/
generateNormalSession(productId: string): EventPayload[] {
return [
this.viewProduct(productId),
this.hover(productId, 'title'),
];
}
/**
* Generate high-velocity agent-like behavior
* This should trigger SessionAwarePricer's agent detection
*/
generateAgentBehavior(productIds: string[]): EventPayload[] {
const events: EventPayload[] = [];
// Rapid-fire product views across multiple products
for (const productId of productIds) {
events.push(this.viewProduct(productId));
// Very quick succession - agent-like behavior
}
return events;
}
/**
* Generate a price log entry
*/
createPriceLog(productId: string, price: number): PriceLogPayload {
return {
productId,
price,
sessionId: this.sessionId,
experimentId: this.experimentId,
storeMode: this.storeMode,
ts: new Date().toISOString(),
};
}
}
/**
* Pre-configured test products for E2E tests
* These should match products in your test database
*/
export const TestProducts = {
// Hotel products with known base prices
hotel1: {
id: 'e2e-test-hotel-001',
basePrice: 150.00,
storeMode: 'hotel' as const,
name: 'E2E Test Hotel 1',
},
hotel2: {
id: 'e2e-test-hotel-002',
basePrice: 200.00,
storeMode: 'hotel' as const,
name: 'E2E Test Hotel 2',
},
hotel3: {
id: 'e2e-test-hotel-003',
basePrice: 100.00,
storeMode: 'hotel' as const,
name: 'E2E Test Hotel 3',
},
// Airline products
airline1: {
id: 'e2e-test-airline-001',
basePrice: 350.00,
storeMode: 'airline' as const,
name: 'E2E Test Flight 1',
},
};
/**
* Generate a unique test product ID for isolation
*/
export function generateTestProductId(prefix: string = 'e2e-test'): string {
return `${prefix}-${uuidv4().slice(0, 8)}`;
}