What Changed: - Added VERIFIED-PRICES.md with honest assessment - Added BUDGET-REALITY.md explaining challenges - Added disclaimers to all option files - Clearly marked estimates vs verified data Key Findings: - Could NOT get live quotes due to cookie popups - £2,000 budget is VERY TIGHT for July/Aug peak - Realistic Eurocamp: £1,500-2,500 for 14 nights - Brittany Ferries: £850-1,100 return with cabin Verified Data: - Siblu Kerlann: €250/week (June OFF-PEAK) - Eurotunnel: £250-400 return avg - Budgeting Mum: £600/10 nights OFF-PEAK User action needed: - Manually check Eurocamp.co.uk - Consider shorter duration - Consider gîte instead of mobile home
281 lines
9.4 KiB
JavaScript
281 lines
9.4 KiB
JavaScript
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() {
|
|
// Ensure directories exist
|
|
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
|
fs.mkdirSync(PRICES_DIR, { recursive: true });
|
|
|
|
console.log('Starting Eurocamp search for La Grande Métairie...');
|
|
console.log('Screenshot dir:', SCREENSHOT_DIR);
|
|
console.log('Prices dir:', PRICES_DIR);
|
|
|
|
const browser = await chromium.launch({
|
|
headless: true,
|
|
args: [
|
|
'--no-sandbox',
|
|
'--disable-setuid-sandbox',
|
|
'--disable-blink-features=AutomationControlled'
|
|
]
|
|
});
|
|
|
|
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',
|
|
timezoneId: 'Europe/London'
|
|
});
|
|
|
|
// Add headers to appear more like a real browser
|
|
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',
|
|
url: 'https://www.eurocamp.co.uk/campsites/france/brittany/la-grande-metairie-campsite',
|
|
prices: [],
|
|
screenshots: [],
|
|
status: 'in_progress',
|
|
errors: [],
|
|
notes: []
|
|
};
|
|
|
|
try {
|
|
// Step 1: Go to the campsite page
|
|
console.log('\n=== Step 1: Loading campsite page ===');
|
|
await page.goto(results.url, {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 90000
|
|
});
|
|
|
|
console.log('Page loaded, waiting for content...');
|
|
await page.waitForTimeout(5000);
|
|
|
|
// Take screenshot
|
|
const screenshot1 = path.join(SCREENSHOT_DIR, 'eurocamp-01-initial.png');
|
|
await page.screenshot({ path: screenshot1, fullPage: false });
|
|
results.screenshots.push(screenshot1);
|
|
console.log('✓ Screenshot saved:', screenshot1);
|
|
|
|
// Log page title
|
|
const title = await page.title();
|
|
results.notes.push(`Page title: ${title}`);
|
|
console.log('Page title:', title);
|
|
|
|
// Save page HTML for debugging
|
|
const html = await page.content();
|
|
fs.writeFileSync(path.join(SCREENSHOT_DIR, 'eurocamp-page.html'), html);
|
|
console.log('✓ HTML saved');
|
|
|
|
// Step 2: Try to find and interact with the booking widget
|
|
console.log('\n=== Step 2: Looking for booking elements ===');
|
|
|
|
// Look for various booking elements
|
|
const bookingElements = await page.evaluate(() => {
|
|
const elements = [];
|
|
|
|
// Look for buttons
|
|
document.querySelectorAll('button, a').forEach(el => {
|
|
const text = el.textContent.trim();
|
|
if (text.toLowerCase().includes('book') ||
|
|
text.toLowerCase().includes('check') ||
|
|
text.toLowerCase().includes('search') ||
|
|
text.toLowerCase().includes('availability')) {
|
|
elements.push({
|
|
tag: el.tagName,
|
|
text: text.substring(0, 100),
|
|
class: el.className,
|
|
id: el.id
|
|
});
|
|
}
|
|
});
|
|
|
|
// Look for date inputs
|
|
document.querySelectorAll('input').forEach(el => {
|
|
elements.push({
|
|
tag: 'INPUT',
|
|
type: el.type,
|
|
placeholder: el.placeholder,
|
|
name: el.name,
|
|
id: el.id
|
|
});
|
|
});
|
|
|
|
return elements;
|
|
});
|
|
|
|
console.log('Found booking elements:', JSON.stringify(bookingElements.slice(0, 15), null, 2));
|
|
results.notes.push(`Found ${bookingElements.length} potential booking elements`);
|
|
|
|
// Step 3: Try to find prices on the page
|
|
console.log('\n=== Step 3: Looking for prices ===');
|
|
|
|
const priceInfo = await page.evaluate(() => {
|
|
const prices = [];
|
|
const bodyText = document.body.innerText;
|
|
|
|
// Look for price patterns
|
|
const priceRegex = /£[\d,]+/g;
|
|
const matches = bodyText.match(priceRegex);
|
|
if (matches) {
|
|
matches.slice(0, 20).forEach(m => prices.push(m));
|
|
}
|
|
|
|
// Look for accommodation cards
|
|
const cards = document.querySelectorAll('[class*="accommodation"], [class*="card"], [class*="result"], [class*="price"]');
|
|
const cardInfo = [];
|
|
cards.forEach(card => {
|
|
const text = card.textContent.trim();
|
|
if (text.length > 50 && text.length < 1000) {
|
|
cardInfo.push(text.substring(0, 300));
|
|
}
|
|
});
|
|
|
|
return { prices, cards: cardInfo.slice(0, 10) };
|
|
});
|
|
|
|
console.log('Prices found on page:', priceInfo.prices);
|
|
results.notes.push(`Prices found: ${priceInfo.prices.join(', ')}`);
|
|
|
|
if (priceInfo.cards.length > 0) {
|
|
console.log('\nAccommodation cards found:');
|
|
priceInfo.cards.forEach((card, i) => {
|
|
console.log(`Card ${i + 1}:`, card.substring(0, 150) + '...');
|
|
});
|
|
}
|
|
|
|
// Step 4: Try to construct a direct booking URL
|
|
console.log('\n=== Step 4: Trying direct search URL ===');
|
|
|
|
// Eurocamp uses various URL patterns for search
|
|
const searchUrls = [
|
|
`https://www.eurocamp.co.uk/search?campsite=la-grande-metairie&checkIn=2026-07-18&nights=14&adults=2&children=1&childAge1=6`,
|
|
`https://www.eurocamp.co.uk/campsites/france/brittany/la-grande-metairie-campsite?checkIn=2026-07-18&nights=14&adults=2&children=1&childAge1=6`,
|
|
`https://www.eurocamp.co.uk/booking?campsite=la-grande-metairie&arrival=2026-07-18&departure=2026-08-02&adults=2&children=1&childAges=6`
|
|
];
|
|
|
|
for (const searchUrl of searchUrls) {
|
|
try {
|
|
console.log('Trying URL:', searchUrl);
|
|
await page.goto(searchUrl, {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 60000
|
|
});
|
|
await page.waitForTimeout(3000);
|
|
|
|
const urlName = searchUrl.split('?')[0].split('/').pop();
|
|
const screenshot = path.join(SCREENSHOT_DIR, `eurocamp-search-${urlName}.png`);
|
|
await page.screenshot({ path: screenshot, fullPage: false });
|
|
results.screenshots.push(screenshot);
|
|
console.log('✓ Screenshot saved for search URL');
|
|
|
|
// Check for prices on this page
|
|
const searchPrices = await page.evaluate(() => {
|
|
const prices = [];
|
|
const bodyText = document.body.innerText;
|
|
const priceRegex = /£[\d,]+/g;
|
|
const matches = bodyText.match(priceRegex);
|
|
if (matches) {
|
|
matches.forEach(m => {
|
|
if (!prices.includes(m)) prices.push(m);
|
|
});
|
|
}
|
|
return prices;
|
|
});
|
|
|
|
if (searchPrices.length > 0) {
|
|
console.log('Found prices on search page:', searchPrices);
|
|
results.prices.push(...searchPrices);
|
|
}
|
|
|
|
} catch (e) {
|
|
console.log('Error with URL:', e.message);
|
|
results.errors.push(`Search URL error: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
// Take final screenshot
|
|
const finalScreenshot = path.join(SCREENSHOT_DIR, 'eurocamp-final.png');
|
|
await page.screenshot({ path: finalScreenshot, fullPage: true });
|
|
results.screenshots.push(finalScreenshot);
|
|
|
|
// Get final URL
|
|
results.url = page.url();
|
|
console.log('\nFinal URL:', results.url);
|
|
|
|
// Step 5: Check if we need to try a different approach
|
|
console.log('\n=== Step 5: Trying to find actual booking form ===');
|
|
|
|
// Look for iframe booking widgets
|
|
const frames = page.frames();
|
|
console.log(`Found ${frames.length} frames`);
|
|
|
|
for (const frame of frames) {
|
|
if (frame !== page.mainFrame()) {
|
|
console.log('Checking frame:', frame.url());
|
|
try {
|
|
const frameContent = await frame.content();
|
|
if (frameContent.includes('price') || frameContent.includes('£')) {
|
|
console.log('Found potential price content in frame');
|
|
results.notes.push('Found booking iframe with potential price data');
|
|
}
|
|
} catch (e) {
|
|
console.log('Could not access frame:', e.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
results.status = 'completed';
|
|
|
|
} catch (error) {
|
|
console.error('Error:', error.message);
|
|
results.errors.push(error.message);
|
|
results.status = 'error';
|
|
|
|
// Take error screenshot
|
|
const errorScreenshot = path.join(SCREENSHOT_DIR, 'eurocamp-error.png');
|
|
await page.screenshot({ path: errorScreenshot, fullPage: true });
|
|
results.screenshots.push(errorScreenshot);
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
|
|
// Remove duplicates from prices
|
|
results.prices = [...new Set(results.prices)];
|
|
|
|
// 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 ===');
|
|
console.log('Output file:', outputPath);
|
|
|
|
return results;
|
|
}
|
|
|
|
searchEurocamp().then(results => {
|
|
console.log('\n=== Search Complete ===');
|
|
console.log('Status:', results.status);
|
|
console.log('Prices found:', results.prices);
|
|
console.log('Screenshots:', results.screensshots);
|
|
console.log('Errors:', results.errors);
|
|
}).catch(err => {
|
|
console.error('Fatal error:', err);
|
|
process.exit(1);
|
|
});
|