mirror of
https://github.com/velocitatem/PHANTOM.git
synced 2026-06-01 00:53:36 +00:00
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
This commit is contained in:
249
e2e/lib/event-generator.ts
Normal file
249
e2e/lib/event-generator.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
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)}`;
|
||||
}
|
||||
Reference in New Issue
Block a user