mirror of
https://github.com/velocitatem/PHANTOM.git
synced 2026-05-31 08:33:36 +00:00
* a simp0le scaffold * feature: simple npm setup * feature: testing setup and dummy scenarios * chore: dumping kafak just via backend * chore: dcleaning gitignore * features: boilerplate fixtures and stuff * test: extra tests * chore: update the test suite to be callable via makefile * chore: cleaning * chore: updating interactions setup * small cleaning * chore: cleaning shitty code
220 lines
7.2 KiB
TypeScript
220 lines
7.2 KiB
TypeScript
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(`/products/${productId}`);
|
|
await page.waitForLoadState('networkidle');
|
|
}
|
|
|
|
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, count: number, delayMs: number = 100, storeType: 'hotel' | 'airline' = 'hotel'): Promise<string[]> {
|
|
const productIds: string[] = [];
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
const productId = await viewProductViaFlow(page, storeType);
|
|
productIds.push(productId);
|
|
|
|
await page.waitForTimeout(delayMs);
|
|
}
|
|
|
|
return productIds;
|
|
}
|
|
|
|
export async function humanLikeViewProduct(page: Page, storeType: 'hotel' | 'airline' = 'hotel'
|
|
): Promise<string> {
|
|
const productId = await viewProductViaFlow(page, 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);
|
|
}
|
|
|
|
return productId;
|
|
}
|
|
|
|
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);
|
|
}
|