Files
PHANTOM/e2e/lib/fixtures.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

144 lines
4.2 KiB
TypeScript

import { test as base, expect } from '@playwright/test';
import { PhantomApiClient, apiClient } from './api-client';
import { EventGenerator, TestProducts } from './event-generator';
import { runPricingPipeline, waitForPriceUpdate, PipelineResult } from './pipeline-runner';
import { testConfig } from '../playwright.config';
/**
* Extended test fixtures for PHANTOM E2E tests
*/
export interface PhantomTestFixtures {
/** API client for interacting with PHANTOM services */
api: PhantomApiClient;
/** Event generator for creating test events */
events: EventGenerator;
/** Run the pricing pipeline and wait for updates */
triggerPriceUpdate: (options?: {
storeMode?: 'hotel' | 'airline';
highThreshold?: number;
lowThreshold?: number;
surgeMultiplier?: number;
discountMultiplier?: number;
}) => Promise<PipelineResult>;
/** Wait for a specific price condition */
waitForPrice: (
productId: string,
condition: (price: number, basePrice: number) => boolean,
storeMode?: 'hotel' | 'airline'
) => Promise<{ price: number; basePrice: number; markup: number }>;
/** Test configuration */
config: typeof testConfig;
}
/**
* Custom test with PHANTOM fixtures
*/
export const test = base.extend<PhantomTestFixtures>({
api: async ({}, use) => {
await use(apiClient);
},
events: async ({}, use) => {
// Create a new event generator with a fresh session for each test
const generator = new EventGenerator({
storeMode: 'hotel',
});
await use(generator);
},
triggerPriceUpdate: async ({}, use) => {
const trigger = async (options = {}) => {
const result = await runPricingPipeline({
storeMode: 'hotel',
highThreshold: testConfig.pricing.highThreshold,
lowThreshold: testConfig.pricing.lowThreshold,
surgeMultiplier: testConfig.pricing.surgeMultiplier,
discountMultiplier: testConfig.pricing.discountMultiplier,
...options,
});
// Wait a moment for Redis to be fully updated
await new Promise(resolve => setTimeout(resolve, 500));
return result;
};
await use(trigger);
},
waitForPrice: async ({ api }, use) => {
const waiter = async (
productId: string,
condition: (price: number, basePrice: number) => boolean,
storeMode: 'hotel' | 'airline' = 'hotel'
) => {
let lastPrice = 0;
let lastBasePrice = 0;
const updated = await waitForPriceUpdate(async () => {
const priceResponse = await api.getPrice(storeMode, productId);
lastPrice = priceResponse.price;
lastBasePrice = priceResponse.base_price;
return condition(priceResponse.price, priceResponse.base_price);
});
if (!updated) {
throw new Error(
`Price condition not met within timeout. Last price: ${lastPrice}, base: ${lastBasePrice}`
);
}
return {
price: lastPrice,
basePrice: lastBasePrice,
markup: lastPrice / lastBasePrice,
};
};
await use(waiter);
},
config: async ({}, use) => {
await use(testConfig);
},
});
export { expect };
/**
* Helper assertions for pricing tests
*/
export const PricingAssertions = {
/**
* Assert that a price has surge markup applied
*/
isSurged: (price: number, basePrice: number, expectedMultiplier: number, tolerance = 0.01) => {
const actualMarkup = price / basePrice;
const minExpected = expectedMultiplier * (1 - tolerance);
const maxExpected = expectedMultiplier * (1 + tolerance);
return actualMarkup >= minExpected && actualMarkup <= maxExpected;
},
/**
* Assert that a price has discount applied
*/
isDiscounted: (price: number, basePrice: number, expectedMultiplier: number, tolerance = 0.01) => {
const actualMarkup = price / basePrice;
const minExpected = expectedMultiplier * (1 - tolerance);
const maxExpected = expectedMultiplier * (1 + tolerance);
return actualMarkup >= minExpected && actualMarkup <= maxExpected;
},
/**
* Assert that a price is at base (no surge/discount)
*/
isBase: (price: number, basePrice: number, tolerance = 0.01) => {
const actualMarkup = price / basePrice;
return actualMarkup >= (1 - tolerance) && actualMarkup <= (1 + tolerance);
},
};