const { chromium } = require('playwright'); const fs = require('fs'); const path = require('path'); const os = require('os'); const BASE_DIR = path.join(os.homedir(), 'holiday-planning'); const SCREENSHOT_DIR = path.join(BASE_DIR, 'price-evidence'); const PRICES_DIR = path.join(BASE_DIR, 'prices'); async function searchEurocamp() { fs.mkdirSync(SCREENSHOT_DIR, { recursive: true }); fs.mkdirSync(PRICES_DIR, { recursive: true }); console.log('=== Eurocamp Price Search for La Grande Métairie ==='); console.log('Dates: 18 July 2026 - 2 August 2026 (14 nights)'); console.log('Guests: 2 adults, 1 child (age 6)\n'); const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] }); const context = await browser.newContext({ viewport: { width: 1920, height: 1080 }, userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', locale: 'en-GB' }); await context.addInitScript(() => { Object.defineProperty(navigator, 'webdriver', { get: () => false }); }); const page = await context.newPage(); const results = { searchDate: new Date().toISOString(), checkIn: '2026-07-18', checkOut: '2026-08-02', nights: 14, adults: 2, children: 1, childAge: 6, campsite: 'La Grande Métairie', campsiteUrl: 'https://www.eurocamp.co.uk/campsites/france/brittany/la-grande-metairie-campsite', finalQuoteUrl: null, accommodations: [], screenshots: [], status: 'in_progress', errors: [] }; try { // Step 1: Load the campsite page console.log('Step 1: Loading campsite page...'); await page.goto(results.campsiteUrl, { waitUntil: 'domcontentloaded', timeout: 90000 }); await page.waitForTimeout(3000); await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'step1-campsite-page.png') }); results.screenshots.push('step1-campsite-page.png'); // Step 2: Click "Prices & availability" button console.log('\nStep 2: Looking for "Prices & availability" button...'); const pricesButton = await page.waitForSelector('button:has-text("Prices & availability")', { timeout: 10000 }); if (pricesButton) { console.log('✓ Found "Prices & availability" button, clicking...'); await pricesButton.click(); await page.waitForTimeout(2000); await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'step2-after-prices-click.png') }); results.screenshots.push('step2-after-prices-click.png'); } // Step 3: Look for the booking widget/form console.log('\nStep 3: Looking for booking widget...'); // The booking widget might be in a modal or expandable section // Look for date inputs await page.waitForTimeout(2000); // Try to find date input fields const dateInputs = await page.$$('input[type="text"]'); console.log(`Found ${dateInputs.length} text inputs`); // Look for specific input patterns const checkInSelectors = [ 'input[placeholder*="Check in"]', 'input[placeholder*="Check-in"]', 'input[placeholder*="Arrival"]', 'input[name*="checkIn"]', 'input[name*="arrival"]', '[data-testid*="checkin"]', '[data-testid*="check-in"]' ]; let checkInInput = null; for (const selector of checkInSelectors) { checkInInput = await page.$(selector); if (checkInInput) { console.log(`✓ Found check-in input: ${selector}`); break; } } if (!checkInInput) { // Try clicking on a date picker trigger console.log('Looking for date picker trigger...'); const dateTriggers = await page.$$('button, [role="button"], .date-picker'); for (const trigger of dateTriggers) { const text = await trigger.textContent(); if (text && (text.includes('Select date') || text.includes('Choose date') || text.includes('Arrival'))) { console.log('Found date trigger:', text.substring(0, 50)); await trigger.click(); await page.waitForTimeout(1000); break; } } } await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'step3-booking-widget.png'), fullPage: true }); results.screenshots.push('step3-booking-widget.png'); // Step 4: Try to use a direct booking URL with parameters console.log('\nStep 4: Trying direct booking URL with parameters...'); // Eurocamp typically has a booking flow. Let's try to access it directly const bookingUrl = 'https://www.eurocamp.co.uk/campsites/france/brittany/la-grande-metairie-campsite/book'; try { await page.goto(bookingUrl, { waitUntil: 'domcontentloaded', timeout: 60000 }); await page.waitForTimeout(3000); await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'step4-booking-page.png'), fullPage: true }); results.screenshots.push('step4-booking-page.png'); // Check if we're on a booking page const url = page.url(); console.log('Current URL:', url); // Look for date pickers and guest selectors const bookingInputs = await page.evaluate(() => { const inputs = []; document.querySelectorAll('input, select').forEach(el => { inputs.push({ tag: el.tagName, type: el.type, name: el.name, placeholder: el.placeholder, id: el.id, value: el.value }); }); return inputs; }); console.log('Booking inputs found:', JSON.stringify(bookingInputs, null, 2)); } catch (e) { console.log('Could not access booking page directly:', e.message); } // Step 5: Try the Eurocamp search with proper parameters console.log('\nStep 5: Trying search with date parameters...'); // Eurocamp search URL pattern const searchUrl = `https://www.eurocamp.co.uk/search?adults=2&children=1&childAge1=6&duration=14&arrivalDate=2026-07-18&parcs=la-grande-metairie`; await page.goto(searchUrl, { waitUntil: 'domcontentloaded', timeout: 60000 }); await page.waitForTimeout(5000); await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'step5-search-results.png'), fullPage: true }); results.screenshots.push('step5-search-results.png'); results.finalQuoteUrl = page.url(); console.log('Search URL:', results.finalQuoteUrl); // Extract accommodation options and prices console.log('\nStep 6: Extracting accommodation prices...'); const accommodations = await page.evaluate(() => { const accs = []; // Look for accommodation cards const cards = document.querySelectorAll('[class*="accommodation"], [class*="Accommodation"], [class*="result-card"], [class*="ResultCard"]'); cards.forEach(card => { try { // Try to extract accommodation name const nameEl = card.querySelector('h2, h3, [class*="name"], [class*="title"]'); const name = nameEl ? nameEl.textContent.trim() : ''; // Try to extract price const priceEl = card.querySelector('[class*="price"], [class*="Price"]'); const priceText = priceEl ? priceEl.textContent.trim() : ''; // Extract all text to find prices const fullText = card.textContent; const priceMatch = fullText.match(/£[\d,]+(?:\.\d{2})?/g); if (name || priceMatch) { accs.push({ name: name, priceText: priceText, prices: priceMatch || [], snippet: fullText.substring(0, 300) }); } } catch (e) {} }); return accs; }); console.log(`Found ${accommodations.length} accommodations`); accommodations.forEach((acc, i) => { console.log(`\n${i + 1}. ${acc.name || 'Unknown'}`); console.log(` Prices: ${acc.prices.join(', ')}`); results.accommodations.push(acc); }); // Also get all prices from the page const allPrices = await page.evaluate(() => { const prices = []; const text = document.body.innerText; const matches = text.match(/£[\d,]+(?:\.\d{2})?/g); return matches ? [...new Set(matches)] : []; }); console.log('\nAll unique prices found on page:', allPrices.slice(0, 20).join(', ')); // Save the HTML for debugging const html = await page.content(); fs.writeFileSync(path.join(SCREENSHOT_DIR, 'final-page.html'), html); results.status = 'completed'; results.allPrices = allPrices; } catch (error) { console.error('Error:', error.message); results.errors.push(error.message); results.status = 'error'; await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'error-screenshot.png'), fullPage: true }); results.screenshots.push('error-screenshot.png'); } finally { await browser.close(); } // Save results const outputPath = path.join(PRICES_DIR, 'eurocamp-la-grande-metairie.json'); fs.writeFileSync(outputPath, JSON.stringify(results, null, 2)); console.log('\n=== Results saved to:', outputPath); return results; } searchEurocamp().catch(err => { console.error('Fatal error:', err); process.exit(1); });