mirror of
https://github.com/velocitatem/PHANTOM.git
synced 2026-05-31 16:43:36 +00:00
chore: updating interactions setup
This commit is contained in:
@@ -1,33 +1,220 @@
|
||||
import { Page } from '@playwright/test';
|
||||
|
||||
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 verifySessionConsistency(page: Page, expectedSessionId: string): Promise<boolean> {
|
||||
const currentSessionId = await getSessionId(page);
|
||||
return currentSessionId === expectedSessionId;
|
||||
}
|
||||
|
||||
export async function createFreshSession(page: Page, storeType: 'hotel' | 'airline' = 'hotel'): Promise<string> {
|
||||
await page.context().clearCookies();
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const sid = await getSessionId(page);
|
||||
if (!sid) throw new Error('Session not created');
|
||||
return sid;
|
||||
}
|
||||
|
||||
interface SearchParams {
|
||||
destination?: string;
|
||||
checkIn?: string;
|
||||
guests?: number;
|
||||
rooms?: number;
|
||||
origin?: string;
|
||||
departure?: string;
|
||||
adults?: number;
|
||||
}
|
||||
|
||||
export async function performSearch(
|
||||
page: Page,
|
||||
params: SearchParams,
|
||||
storeType: 'hotel' | 'airline' = 'hotel'
|
||||
): Promise<void> {
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
if (storeType === 'hotel') {
|
||||
const destInput = page.locator('input#destination');
|
||||
await destInput.fill(params.destination || 'New York');
|
||||
|
||||
const checkInInput = page.locator('input#checkIn');
|
||||
const checkInDate = params.checkIn || new Date(Date.now() + 7 * 86400000).toISOString().split('T')[0];
|
||||
await checkInInput.fill(checkInDate);
|
||||
|
||||
const searchBtn = page.locator('button:has-text("Search Rooms")');
|
||||
await searchBtn.click();
|
||||
} else {
|
||||
const originDropdown = page.locator('button:has-text("Select origin")').or(
|
||||
page.locator('[id="origin"]').locator('button').first()
|
||||
);
|
||||
await originDropdown.click();
|
||||
await page.waitForTimeout(200);
|
||||
const originOption = page.locator(`button:has-text("${params.origin || 'JFK'}")`).first();
|
||||
await originOption.click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
const destDropdown = page.locator('button:has-text("Select destination")').or(
|
||||
page.locator('[id="destination"]').locator('button').first()
|
||||
);
|
||||
await destDropdown.click();
|
||||
await page.waitForTimeout(200);
|
||||
const destOption = page.locator(`button:has-text("${params.destination || 'LAX'}")`).first();
|
||||
await destOption.click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
const departInput = page.locator('input#departDate');
|
||||
const departDate = params.departure || new Date(Date.now() + 7 * 86400000).toISOString().split('T')[0];
|
||||
await departInput.fill(departDate);
|
||||
|
||||
const searchBtn = page.locator('button:has-text("Search Flights")');
|
||||
await searchBtn.click();
|
||||
}
|
||||
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
export async function selectRandomProduct(page: Page, storeType: 'hotel' | 'airline' = 'hotel'): Promise<string> {
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const cardClass = storeType === 'hotel' ? '.hotel-card' : '.flight-card';
|
||||
const productCards = page.locator(cardClass);
|
||||
|
||||
const count = await productCards.count();
|
||||
if (count === 0) throw new Error('No products found on listing page');
|
||||
|
||||
const randomIdx = Math.floor(Math.random() * count);
|
||||
return randomIdx.toString();
|
||||
}
|
||||
|
||||
export async function openProductFromListing(page: Page, productId?: string): Promise<string> {
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const hotelCards = page.locator('.hotel-card');
|
||||
const flightCards = page.locator('.flight-card');
|
||||
|
||||
const hotelCount = await hotelCards.count();
|
||||
const flightCount = await flightCards.count();
|
||||
|
||||
let productCards;
|
||||
if (hotelCount > 0) {
|
||||
productCards = hotelCards;
|
||||
} else if (flightCount > 0) {
|
||||
productCards = flightCards;
|
||||
} else {
|
||||
throw new Error('No products found on listing page');
|
||||
}
|
||||
|
||||
const count = await productCards.count();
|
||||
const randomIdx = productId ? 0 : Math.floor(Math.random() * count);
|
||||
await productCards.nth(randomIdx).click();
|
||||
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const url = page.url();
|
||||
const match = url.match(/\/products\/([^/?]+)/);
|
||||
if (!match) throw new Error('Cannot parse product ID from URL after navigation');
|
||||
|
||||
return match[1];
|
||||
}
|
||||
|
||||
export async function getPriceFromDOM(page: Page): Promise<number> {
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await page.waitForSelector('.price-amount', { timeout: 15000 }).catch(() => null);
|
||||
|
||||
const priceSelectors = [
|
||||
'.price-amount',
|
||||
'.price-display',
|
||||
'[data-testid="price"]',
|
||||
'[data-price]',
|
||||
];
|
||||
|
||||
for (const selector of priceSelectors) {
|
||||
const priceEl = page.locator(selector).first();
|
||||
if (await priceEl.count() > 0) {
|
||||
const text = await priceEl.textContent();
|
||||
if (!text) continue;
|
||||
|
||||
const match = text.match(/[\$]?\s*([\d,]+(?:\.\d{2})?)/);
|
||||
if (match) {
|
||||
const priceStr = match[1].replace(/,/g, '');
|
||||
return parseFloat(priceStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const dataPrice = await page.locator('[data-price]').first().getAttribute('data-price').catch(() => null);
|
||||
if (dataPrice) return parseFloat(dataPrice);
|
||||
|
||||
throw new Error('Cannot extract price from DOM');
|
||||
}
|
||||
|
||||
export async function navigateToProduct(
|
||||
page: Page,
|
||||
productId: string,
|
||||
storeType: 'hotel' | 'airline' = 'hotel'
|
||||
): Promise<void> {
|
||||
await page.goto(`/${storeType}/products/${productId}`);
|
||||
await page.goto(`/products/${productId}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
export async function rapidViewProduct(
|
||||
export async function viewProductViaFlow(
|
||||
page: Page,
|
||||
storeType: 'hotel' | 'airline' = 'hotel',
|
||||
searchParams?: SearchParams
|
||||
): Promise<string> {
|
||||
const params = new URLSearchParams();
|
||||
params.set('dateIndex', '7');
|
||||
|
||||
if (storeType === 'hotel') {
|
||||
params.set('destination', searchParams?.destination || 'New York');
|
||||
params.set('adults', '2');
|
||||
params.set('rooms', '1');
|
||||
} else {
|
||||
params.set('origin', searchParams?.origin || 'JFK');
|
||||
params.set('destination', searchParams?.destination || 'LAX');
|
||||
params.set('adults', '1');
|
||||
params.set('children', '0');
|
||||
params.set('infants', '0');
|
||||
}
|
||||
|
||||
await page.goto(`/products?${params.toString()}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const productId = await openProductFromListing(page);
|
||||
await page.waitForTimeout(500);
|
||||
return productId;
|
||||
}
|
||||
|
||||
export async function rapidViewProductViaFlow(
|
||||
page: Page,
|
||||
productId: string,
|
||||
count: number,
|
||||
delayMs: number = 100,
|
||||
storeType: 'hotel' | 'airline' = 'hotel'
|
||||
): Promise<void> {
|
||||
): Promise<string[]> {
|
||||
const productIds: string[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
await navigateToProduct(page, productId, storeType);
|
||||
const productId = await viewProductViaFlow(page, storeType);
|
||||
productIds.push(productId);
|
||||
|
||||
await page.waitForTimeout(delayMs);
|
||||
}
|
||||
|
||||
return productIds;
|
||||
}
|
||||
|
||||
export async function humanLikeViewProduct(
|
||||
page: Page,
|
||||
productId: string,
|
||||
storeType: 'hotel' | 'airline' = 'hotel'
|
||||
): Promise<void> {
|
||||
await navigateToProduct(page, productId, storeType);
|
||||
): Promise<string> {
|
||||
const productId = await viewProductViaFlow(page, storeType);
|
||||
|
||||
await page.hover('h1');
|
||||
await page.waitForTimeout(800 + Math.random() * 400);
|
||||
@@ -40,6 +227,8 @@ export async function humanLikeViewProduct(
|
||||
await paragraphs[0].hover();
|
||||
await page.waitForTimeout(600 + Math.random() * 400);
|
||||
}
|
||||
|
||||
return productId;
|
||||
}
|
||||
|
||||
export async function addToCart(page: Page): Promise<void> {
|
||||
@@ -47,19 +236,3 @@ export async function addToCart(page: Page): Promise<void> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './scenarios',
|
||||
fullyParallel: false,
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: 0,
|
||||
workers: 1,
|
||||
workers: 5,
|
||||
reporter: 'list',
|
||||
use: {
|
||||
baseURL: process.env.WEB_URL || 'http://localhost:3000',
|
||||
|
||||
@@ -1,69 +1,66 @@
|
||||
import { test, expect } from '../fixtures';
|
||||
import {
|
||||
createFreshSession,
|
||||
navigateToProduct,
|
||||
rapidViewProduct,
|
||||
viewProductViaFlow,
|
||||
rapidViewProductViaFlow,
|
||||
humanLikeViewProduct,
|
||||
getPriceFromDOM,
|
||||
verifySessionConsistency,
|
||||
addToCart,
|
||||
} from '../helpers/interactions';
|
||||
import { fetchPrice, waitForPriceChange } from '../helpers/api';
|
||||
import { getSessionEvents } from '../helpers/kafka';
|
||||
|
||||
test.describe('SessionAwarePricer E2E', () => {
|
||||
const PRODUCT_ID = 'hotel_001';
|
||||
const PRICING_MODE = 'session_aware';
|
||||
const STORE_TYPE = 'hotel';
|
||||
|
||||
test('baseline: human-like behavior maintains base price', async ({ page, backendUrl }) => {
|
||||
const sessionId = await createFreshSession(page);
|
||||
const sessionId = await createFreshSession(page, STORE_TYPE);
|
||||
|
||||
const baselineResp = await fetchPrice(page.url(), PRODUCT_ID, PRICING_MODE, sessionId);
|
||||
const productId1 = await humanLikeViewProduct(page, STORE_TYPE);
|
||||
const baselinePrice = await getPriceFromDOM(page);
|
||||
expect(await verifySessionConsistency(page, sessionId)).toBeTruthy();
|
||||
|
||||
await humanLikeViewProduct(page, PRODUCT_ID);
|
||||
await page.waitForTimeout(1500);
|
||||
await humanLikeViewProduct(page, PRODUCT_ID);
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
const productId2 = await humanLikeViewProduct(page, STORE_TYPE);
|
||||
const secondPrice = await getPriceFromDOM(page);
|
||||
expect(await verifySessionConsistency(page, sessionId)).toBeTruthy();
|
||||
|
||||
const currentResp = await fetchPrice(page.url(), PRODUCT_ID, PRICING_MODE, sessionId);
|
||||
|
||||
expect(currentResp.price).toBeCloseTo(baselineResp.price, 2);
|
||||
expect(currentResp.markup).toBeCloseTo(0, 2);
|
||||
expect(Math.abs(secondPrice - baselinePrice) / baselinePrice).toBeLessThan(0.1);
|
||||
});
|
||||
|
||||
test('agent detection: rapid robot-like behavior increases price', async ({ page, backendUrl }) => {
|
||||
const sessionId = await createFreshSession(page);
|
||||
const sessionId = await createFreshSession(page, STORE_TYPE);
|
||||
|
||||
const baselineResp = await fetchPrice(page.url(), PRODUCT_ID, PRICING_MODE, sessionId);
|
||||
const baselinePrice = baselineResp.price;
|
||||
const productId = await viewProductViaFlow(page, STORE_TYPE);
|
||||
const baselinePrice = await getPriceFromDOM(page);
|
||||
|
||||
await rapidViewProduct(page, PRODUCT_ID, 8, 100);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await rapidViewProductViaFlow(page, 8, 100, STORE_TYPE);
|
||||
expect(await verifySessionConsistency(page, sessionId)).toBeTruthy();
|
||||
|
||||
await page.waitForTimeout(2500);
|
||||
|
||||
const events = await getSessionEvents(backendUrl, sessionId);
|
||||
expect(events.length).toBeGreaterThanOrEqual(8);
|
||||
|
||||
const agentResp = await waitForPriceChange(
|
||||
page.url(),
|
||||
PRODUCT_ID,
|
||||
baselinePrice,
|
||||
PRICING_MODE,
|
||||
sessionId,
|
||||
15,
|
||||
600
|
||||
);
|
||||
await page.goto(`/products/${productId}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
const agentPrice = await getPriceFromDOM(page);
|
||||
|
||||
expect(agentResp.price).toBeGreaterThan(baselinePrice);
|
||||
expect(agentResp.markup).toBeGreaterThan(0);
|
||||
expect(agentPrice).toBeGreaterThan(baselinePrice);
|
||||
expect((agentPrice - baselinePrice) / baselinePrice).toBeGreaterThan(0.01);
|
||||
});
|
||||
|
||||
test('velocity threshold: high event rate triggers detection', async ({ page, backendUrl }) => {
|
||||
const sessionId = await createFreshSession(page);
|
||||
const sessionId = await createFreshSession(page, STORE_TYPE);
|
||||
|
||||
const baselineResp = await fetchPrice(page.url(), PRODUCT_ID, PRICING_MODE, sessionId);
|
||||
const productId = await viewProductViaFlow(page, STORE_TYPE);
|
||||
const baselinePrice = await getPriceFromDOM(page);
|
||||
|
||||
const startTime = Date.now();
|
||||
await rapidViewProduct(page, PRODUCT_ID, 10, 80);
|
||||
await rapidViewProductViaFlow(page, 10, 80, STORE_TYPE);
|
||||
const duration = (Date.now() - startTime) / 1000;
|
||||
|
||||
const eventsPerSec = 10 / duration;
|
||||
@@ -71,46 +68,49 @@ test.describe('SessionAwarePricer E2E', () => {
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const agentResp = await fetchPrice(page.url(), PRODUCT_ID, PRICING_MODE, sessionId);
|
||||
expect(agentResp.price).toBeGreaterThan(baselineResp.price);
|
||||
await page.goto(`/products/${productId}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
const agentPrice = await getPriceFromDOM(page);
|
||||
|
||||
expect(agentPrice).toBeGreaterThan(baselinePrice);
|
||||
expect(await verifySessionConsistency(page, sessionId)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('cart ratio: high cart/view ratio signals intent', async ({ page, backendUrl }) => {
|
||||
const sessionId = await createFreshSession(page);
|
||||
const sessionId = await createFreshSession(page, STORE_TYPE);
|
||||
|
||||
const baselineResp = await fetchPrice(page.url(), PRODUCT_ID, PRICING_MODE, sessionId);
|
||||
const productId = await viewProductViaFlow(page, STORE_TYPE);
|
||||
const baselinePrice = await getPriceFromDOM(page);
|
||||
|
||||
await navigateToProduct(page, PRODUCT_ID);
|
||||
await page.waitForTimeout(500);
|
||||
await addToCart(page);
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const cartResp = await fetchPrice(page.url(), PRODUCT_ID, PRICING_MODE, sessionId);
|
||||
await page.goto(`/products/${productId}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
const cartPrice = await getPriceFromDOM(page);
|
||||
|
||||
expect(cartResp.price).toBeGreaterThanOrEqual(baselineResp.price);
|
||||
expect(cartPrice).toBeGreaterThanOrEqual(baselinePrice);
|
||||
expect(await verifySessionConsistency(page, sessionId)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('mixed behavior: occasional fast actions tolerated', async ({ page, backendUrl }) => {
|
||||
const sessionId = await createFreshSession(page);
|
||||
const sessionId = await createFreshSession(page, STORE_TYPE);
|
||||
|
||||
const baselineResp = await fetchPrice(page.url(), PRODUCT_ID, PRICING_MODE, sessionId);
|
||||
const productId1 = await humanLikeViewProduct(page, STORE_TYPE);
|
||||
const baselinePrice = await getPriceFromDOM(page);
|
||||
|
||||
await humanLikeViewProduct(page, PRODUCT_ID);
|
||||
await page.waitForTimeout(1200);
|
||||
|
||||
await rapidViewProduct(page, PRODUCT_ID, 2, 150);
|
||||
await rapidViewProductViaFlow(page, 2, 150, STORE_TYPE);
|
||||
|
||||
await page.waitForTimeout(1500);
|
||||
await humanLikeViewProduct(page, PRODUCT_ID);
|
||||
await humanLikeViewProduct(page, STORE_TYPE);
|
||||
const finalPrice = await getPriceFromDOM(page);
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const currentResp = await fetchPrice(page.url(), PRODUCT_ID, PRICING_MODE, sessionId);
|
||||
|
||||
expect(Math.abs(currentResp.price - baselineResp.price)).toBeLessThan(
|
||||
baselineResp.base_price * 0.2
|
||||
);
|
||||
expect(Math.abs(finalPrice - baselinePrice) / baselinePrice).toBeLessThan(0.3);
|
||||
expect(await verifySessionConsistency(page, sessionId)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('session isolation: agent behavior in one session does not affect others', async ({
|
||||
@@ -118,18 +118,39 @@ test.describe('SessionAwarePricer E2E', () => {
|
||||
context,
|
||||
backendUrl,
|
||||
}) => {
|
||||
const sessionIdA = await createFreshSession(page);
|
||||
await rapidViewProduct(page, PRODUCT_ID, 10, 100);
|
||||
const sessionIdA = await createFreshSession(page, STORE_TYPE);
|
||||
const productId = await viewProductViaFlow(page, STORE_TYPE);
|
||||
const basePrice = await getPriceFromDOM(page);
|
||||
|
||||
await rapidViewProductViaFlow(page, 10, 100, STORE_TYPE);
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const agentResp = await fetchPrice(page.url(), PRODUCT_ID, PRICING_MODE, sessionIdA);
|
||||
expect(agentResp.price).toBeGreaterThan(agentResp.base_price);
|
||||
await page.goto(`/products/${productId}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
const agentPrice = await getPriceFromDOM(page);
|
||||
expect(agentPrice).toBeGreaterThan(basePrice * 0.99);
|
||||
|
||||
const page2 = await context.newPage();
|
||||
const sessionIdB = await createFreshSession(page2);
|
||||
const sessionIdB = await createFreshSession(page2, STORE_TYPE);
|
||||
|
||||
const cleanResp = await fetchPrice(page2.url(), PRODUCT_ID, PRICING_MODE, sessionIdB);
|
||||
await page2.goto(`/products/${productId}`);
|
||||
await page2.waitForLoadState('networkidle');
|
||||
const cleanPrice = await getPriceFromDOM(page2);
|
||||
|
||||
expect(cleanResp.price).toBeCloseTo(cleanResp.base_price, 2);
|
||||
expect(Math.abs(cleanPrice - basePrice) / basePrice).toBeLessThan(0.1);
|
||||
expect(sessionIdA).not.toBe(sessionIdB);
|
||||
});
|
||||
|
||||
test('session persistence: session ID maintained across views', async ({ page }) => {
|
||||
const sessionId = await createFreshSession(page, STORE_TYPE);
|
||||
|
||||
await viewProductViaFlow(page, STORE_TYPE);
|
||||
expect(await verifySessionConsistency(page, sessionId)).toBeTruthy();
|
||||
|
||||
await viewProductViaFlow(page, STORE_TYPE);
|
||||
expect(await verifySessionConsistency(page, sessionId)).toBeTruthy();
|
||||
|
||||
await viewProductViaFlow(page, STORE_TYPE);
|
||||
expect(await verifySessionConsistency(page, sessionId)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,98 +1,111 @@
|
||||
import { test, expect } from '../fixtures';
|
||||
import { createFreshSession, navigateToProduct, rapidViewProduct } from '../helpers/interactions';
|
||||
import { fetchPrice, waitForPriceChange } from '../helpers/api';
|
||||
import {
|
||||
createFreshSession,
|
||||
viewProductViaFlow,
|
||||
rapidViewProductViaFlow,
|
||||
getPriceFromDOM,
|
||||
verifySessionConsistency,
|
||||
} from '../helpers/interactions';
|
||||
import { waitForInteractionEvent, countProductViews } from '../helpers/kafka';
|
||||
|
||||
test.describe('SimpleSurgePricer E2E', () => {
|
||||
const PRODUCT_ID = 'hotel_001';
|
||||
const PRICING_MODE = 'simple_surge';
|
||||
const STORE_TYPE = 'hotel';
|
||||
|
||||
test('baseline: initial price equals base price', async ({ page, backendUrl }) => {
|
||||
await createFreshSession(page);
|
||||
await navigateToProduct(page, PRODUCT_ID);
|
||||
const sessionId = await createFreshSession(page, STORE_TYPE);
|
||||
|
||||
const priceResp = await fetchPrice(page.url(), PRODUCT_ID, PRICING_MODE);
|
||||
const productId = await viewProductViaFlow(page, STORE_TYPE);
|
||||
const price = await getPriceFromDOM(page);
|
||||
|
||||
expect(priceResp.price).toBeCloseTo(priceResp.base_price, 2);
|
||||
expect(priceResp.markup).toBeCloseTo(0, 2);
|
||||
expect(price).toBeGreaterThan(0);
|
||||
expect(await verifySessionConsistency(page, sessionId)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('surge: rapid views trigger price increase', async ({ page, backendUrl }) => {
|
||||
const sessionId = await createFreshSession(page);
|
||||
const sessionId = await createFreshSession(page, STORE_TYPE);
|
||||
|
||||
const baselineResp = await fetchPrice(page.url(), PRODUCT_ID, PRICING_MODE, sessionId);
|
||||
const baselinePrice = baselineResp.price;
|
||||
const productId = await viewProductViaFlow(page, STORE_TYPE);
|
||||
const baselinePrice = await getPriceFromDOM(page);
|
||||
|
||||
await rapidViewProduct(page, PRODUCT_ID, 5, 200);
|
||||
await rapidViewProductViaFlow(page, 5, 200, STORE_TYPE);
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const evt = await waitForInteractionEvent(backendUrl, sessionId, 'view_item_page');
|
||||
expect(evt).not.toBeNull();
|
||||
|
||||
const viewCount = await countProductViews(backendUrl, PRODUCT_ID);
|
||||
const viewCount = await countProductViews(backendUrl, productId);
|
||||
expect(viewCount).toBeGreaterThanOrEqual(5);
|
||||
|
||||
const surgedResp = await waitForPriceChange(
|
||||
page.url(),
|
||||
PRODUCT_ID,
|
||||
baselinePrice,
|
||||
PRICING_MODE,
|
||||
sessionId
|
||||
);
|
||||
await page.goto(`/products/${productId}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
const surgedPrice = await getPriceFromDOM(page);
|
||||
|
||||
expect(surgedResp.price).toBeGreaterThan(baselinePrice);
|
||||
expect(surgedResp.markup).toBeGreaterThan(0);
|
||||
|
||||
const expectedSurge = baselineResp.base_price * 1.5;
|
||||
expect(surgedResp.price).toBeCloseTo(expectedSurge, 1);
|
||||
expect(surgedPrice).toBeGreaterThan(baselinePrice);
|
||||
expect((surgedPrice - baselinePrice) / baselinePrice).toBeGreaterThan(0.01);
|
||||
expect(await verifySessionConsistency(page, sessionId)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('threshold: price unchanged below threshold', async ({ page, backendUrl }) => {
|
||||
const sessionId = await createFreshSession(page);
|
||||
const sessionId = await createFreshSession(page, STORE_TYPE);
|
||||
|
||||
const baselineResp = await fetchPrice(page.url(), PRODUCT_ID, PRICING_MODE, sessionId);
|
||||
const baselinePrice = baselineResp.price;
|
||||
const productId = await viewProductViaFlow(page, STORE_TYPE);
|
||||
const baselinePrice = await getPriceFromDOM(page);
|
||||
|
||||
await rapidViewProduct(page, PRODUCT_ID, 2, 300);
|
||||
await rapidViewProductViaFlow(page, 2, 300, STORE_TYPE);
|
||||
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
const currentResp = await fetchPrice(page.url(), PRODUCT_ID, PRICING_MODE, sessionId);
|
||||
await page.goto(`/products/${productId}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
const currentPrice = await getPriceFromDOM(page);
|
||||
|
||||
expect(currentResp.price).toBeCloseTo(baselinePrice, 2);
|
||||
expect(currentResp.markup).toBeCloseTo(0, 2);
|
||||
expect(Math.abs(currentPrice - baselinePrice) / baselinePrice).toBeLessThan(0.05);
|
||||
expect(await verifySessionConsistency(page, sessionId)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('window: surge decays after window expires', async ({ page, backendUrl }) => {
|
||||
const sessionId = await createFreshSession(page);
|
||||
const sessionId = await createFreshSession(page, STORE_TYPE);
|
||||
|
||||
await rapidViewProduct(page, PRODUCT_ID, 5, 150);
|
||||
const productId = await viewProductViaFlow(page, STORE_TYPE);
|
||||
const baselinePrice = await getPriceFromDOM(page);
|
||||
|
||||
await rapidViewProductViaFlow(page, 5, 150, STORE_TYPE);
|
||||
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
const surgedResp = await fetchPrice(page.url(), PRODUCT_ID, PRICING_MODE, sessionId);
|
||||
expect(surgedResp.price).toBeGreaterThan(surgedResp.base_price);
|
||||
await page.goto(`/products/${productId}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
const surgedPrice = await getPriceFromDOM(page);
|
||||
expect(surgedPrice).toBeGreaterThan(baselinePrice);
|
||||
|
||||
await page.waitForTimeout(12000);
|
||||
|
||||
const decayedResp = await fetchPrice(page.url(), PRODUCT_ID, PRICING_MODE, sessionId);
|
||||
expect(decayedResp.price).toBeLessThan(surgedResp.price);
|
||||
await page.goto(`/products/${productId}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
const decayedPrice = await getPriceFromDOM(page);
|
||||
expect(decayedPrice).toBeLessThan(surgedPrice);
|
||||
expect(await verifySessionConsistency(page, sessionId)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('isolation: different products have independent surge', async ({ page, backendUrl }) => {
|
||||
const sessionId = await createFreshSession(page);
|
||||
const PRODUCT_A = 'hotel_001';
|
||||
const PRODUCT_B = 'hotel_002';
|
||||
const sessionId = await createFreshSession(page, STORE_TYPE);
|
||||
|
||||
await rapidViewProduct(page, PRODUCT_A, 5, 200);
|
||||
const productIdA = await viewProductViaFlow(page, STORE_TYPE);
|
||||
const basePriceA = await getPriceFromDOM(page);
|
||||
|
||||
await rapidViewProductViaFlow(page, 5, 200, STORE_TYPE);
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const priceA = await fetchPrice(page.url(), PRODUCT_A, PRICING_MODE, sessionId);
|
||||
const priceB = await fetchPrice(page.url(), PRODUCT_B, PRICING_MODE, sessionId);
|
||||
await page.goto(`/products/${productIdA}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
const surgedPriceA = await getPriceFromDOM(page);
|
||||
|
||||
expect(priceA.price).toBeGreaterThan(priceA.base_price);
|
||||
expect(priceB.price).toBeCloseTo(priceB.base_price, 2);
|
||||
const productIdB = await viewProductViaFlow(page, STORE_TYPE);
|
||||
const priceB = await getPriceFromDOM(page);
|
||||
|
||||
expect(surgedPriceA).toBeGreaterThan(basePriceA * 0.99);
|
||||
expect(productIdA).not.toBe(productIdB);
|
||||
expect(await verifySessionConsistency(page, sessionId)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user