mirror of
https://github.com/velocitatem/PHANTOM.git
synced 2026-05-31 08:33:36 +00:00
features: boilerplate fixtures and stuff
This commit is contained in:
17
tests/e2e/fixtures.ts
Normal file
17
tests/e2e/fixtures.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { test as base } from '@playwright/test';
|
||||||
|
|
||||||
|
type TestFixtures = {
|
||||||
|
backendUrl: string;
|
||||||
|
pricingUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const test = base.extend<TestFixtures>({
|
||||||
|
backendUrl: async ({}, use) => {
|
||||||
|
await use(process.env.BACKEND_URL || 'http://localhost:5000');
|
||||||
|
},
|
||||||
|
pricingUrl: async ({}, use) => {
|
||||||
|
await use(process.env.PRICING_PROVIDER_URL || 'http://localhost:5001');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export { expect } from '@playwright/test';
|
||||||
69
tests/e2e/helpers/api.ts
Normal file
69
tests/e2e/helpers/api.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
65
tests/e2e/helpers/interactions.ts
Normal file
65
tests/e2e/helpers/interactions.ts
Normal 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;
|
||||||
|
}
|
||||||
63
tests/e2e/helpers/kafka.ts
Normal file
63
tests/e2e/helpers/kafka.ts
Normal 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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user