features: boilerplate fixtures and stuff

This commit is contained in:
2026-01-11 19:58:51 +01:00
parent e249f1f680
commit 4765c2966c
4 changed files with 214 additions and 0 deletions

69
tests/e2e/helpers/api.ts Normal file
View File

@@ -0,0 +1,69 @@
interface PriceResponse {
price: number;
base_price: number;
markup: number;
model_version?: string;
}
export async function fetchPrice(
baseUrl: string,
productId: string,
mode: string = 'simple_surge',
sessionId?: string
): Promise<PriceResponse> {
const params = new URLSearchParams();
if (sessionId) params.set('sessionId', sessionId);
const url = `${baseUrl}/api/pricing?mode=${mode}&productId=${productId}&${params}`;
const resp = await fetch(url);
if (!resp.ok) {
throw new Error(`Price fetch failed: ${resp.status}`);
}
return resp.json();
}
export async function waitForPriceChange(
baseUrl: string,
productId: string,
baselinePrice: number,
mode: string,
sessionId?: string,
maxRetries: number = 10,
pollInterval: number = 500
): Promise<PriceResponse> {
for (let i = 0; i < maxRetries; i++) {
const priceResp = await fetchPrice(baseUrl, productId, mode, sessionId);
if (Math.abs(priceResp.price - baselinePrice) > 0.01) {
return priceResp;
}
await new Promise(r => setTimeout(r, pollInterval));
}
throw new Error(`Price did not change after ${maxRetries} retries`);
}
export async function ingestEvent(
baseUrl: string,
sessionId: string,
event: string,
productId?: string,
metadata?: Record<string, any>
): Promise<void> {
const resp = await fetch(`${baseUrl}/api/ingest`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId,
event,
productId,
timestamp: new Date().toISOString(),
metadata,
}),
});
if (!resp.ok) {
throw new Error(`Event ingest failed: ${resp.status}`);
}
}

View File

@@ -0,0 +1,65 @@
import { Page } from '@playwright/test';
export async function navigateToProduct(
page: Page,
productId: string,
storeType: 'hotel' | 'airline' = 'hotel'
): Promise<void> {
await page.goto(`/${storeType}/products/${productId}`);
await page.waitForLoadState('networkidle');
}
export async function rapidViewProduct(
page: Page,
productId: string,
count: number,
delayMs: number = 100,
storeType: 'hotel' | 'airline' = 'hotel'
): Promise<void> {
for (let i = 0; i < count; i++) {
await navigateToProduct(page, productId, storeType);
await page.waitForTimeout(delayMs);
}
}
export async function humanLikeViewProduct(
page: Page,
productId: string,
storeType: 'hotel' | 'airline' = 'hotel'
): Promise<void> {
await navigateToProduct(page, productId, storeType);
await page.hover('h1');
await page.waitForTimeout(800 + Math.random() * 400);
await page.mouse.wheel(0, 200);
await page.waitForTimeout(500 + Math.random() * 300);
const paragraphs = await page.locator('p').all();
if (paragraphs.length > 0) {
await paragraphs[0].hover();
await page.waitForTimeout(600 + Math.random() * 400);
}
}
export async function addToCart(page: Page): Promise<void> {
const addBtn = page.locator('button:has-text("Add to Cart")');
await addBtn.click();
await page.waitForTimeout(500);
}
export async function getSessionId(page: Page): Promise<string | null> {
const cookies = await page.context().cookies();
const sessionCookie = cookies.find(c => c.name === 'phantom_session_id');
return sessionCookie?.value || null;
}
export async function createFreshSession(page: Page): Promise<string> {
await page.context().clearCookies();
await page.goto('/');
await page.waitForTimeout(500);
const sid = await getSessionId(page);
if (!sid) throw new Error('Session not created');
return sid;
}

View File

@@ -0,0 +1,63 @@
interface InteractionEvent {
sessionId: string;
event: string;
productId?: string;
timestamp: string;
metadata?: Record<string, any>;
}
export async function dumpKafkaTopic(
backendUrl: string,
topic: string
): Promise<any[]> {
const resp = await fetch(`${backendUrl}/api/kafka/dump?topic=${topic}`);
if (!resp.ok) {
throw new Error(`Kafka dump failed: ${resp.status}`);
}
const data = await resp.json();
return data.messages || [];
}
export async function waitForInteractionEvent(
backendUrl: string,
sessionId: string,
eventType: string,
maxRetries: number = 10,
pollInterval: number = 500
): Promise<InteractionEvent | null> {
for (let i = 0; i < maxRetries; i++) {
const msgs = await dumpKafkaTopic(backendUrl, 'user-interactions');
for (const msg of msgs) {
if (msg.sessionId === sessionId && msg.event === eventType) {
return msg as InteractionEvent;
}
}
await new Promise(r => setTimeout(r, pollInterval));
}
return null;
}
export async function countProductViews(
backendUrl: string,
productId: string
): Promise<number> {
const msgs = await dumpKafkaTopic(backendUrl, 'user-interactions');
return msgs.reduce((cnt, msg) => {
if (msg.productId === productId && msg.event === 'view_item_page') {
return cnt + 1;
}
return cnt;
}, 0);
}
export async function getSessionEvents(
backendUrl: string,
sessionId: string
): Promise<InteractionEvent[]> {
const msgs = await dumpKafkaTopic(backendUrl, 'user-interactions');
return msgs.filter(m => m.sessionId === sessionId);
}