Files
holiday-plans/eurocamp-search.js
Sean C a27fcfef61 Honest pricing review with disclaimers
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
2026-03-15 23:18:43 +00:00

289 lines
10 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 });
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 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
});
const page = await context.newPage();
let results = {
searchDate: new Date().toISOString(),
checkIn: '2026-07-18',
checkOut: '2026-08-02',
nights: 14,
adults: 2,
children: 1,
childAge: 6,
campsite: 'Domaine des Ormes',
url: 'https://www.eurocamp.co.uk/campsites/france/northern-brittany/domaine-des-ormes-campsite',
prices: [],
screenshots: [],
status: 'in_progress',
errors: [],
notes: []
};
try {
console.log('Step 1: Navigating to Eurocamp Domaine des Ormes page...');
await page.goto(results.url, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForTimeout(3000);
// Screenshot 1: Initial page
const screenshot1 = path.join(SCREENSHOT_DIR, 'eurocamp-01-initial-page.png');
await page.screenshot({ path: screenshot1, fullPage: false });
results.screenshots.push(screenshot1);
console.log('✓ Screenshot 1: Initial page saved');
// Check page title
const title = await page.title();
console.log('Page title:', title);
results.notes.push(`Page title: ${title}`);
// Look for "Book now" or "Check availability" buttons
console.log('\nStep 2: Looking for booking button...');
const selectors = [
'button:has-text("Book now")',
'a:has-text("Book now")',
'button:has-text("Check availability")',
'a:has-text("Check availability")',
'button:has-text("Search")',
'.booking-button',
'[data-testid="book-button"]'
];
let buttonFound = false;
for (const selector of selectors) {
try {
const button = await page.$(selector);
if (button) {
const buttonText = await button.textContent();
console.log(`Found button: "${buttonText}"`);
await button.click();
buttonFound = true;
await page.waitForTimeout(3000);
break;
}
} catch (e) {
// Continue trying
}
}
if (buttonFound) {
const screenshot2 = path.join(SCREENSHOT_DIR, 'eurocamp-02-after-book-click.png');
await page.screenshot({ path: screenshot2, fullPage: true });
results.screenshots.push(screenshot2);
console.log('✓ Screenshot 2: After book click saved');
}
// Check for date picker modal or booking form
console.log('\nStep 3: Looking for date picker...');
// Look for date inputs with various possible selectors
const datePickerSelectors = [
'input[placeholder*="Check in"]',
'input[placeholder*="Arrival"]',
'input[name*="checkIn"]',
'input[name*="check-in"]',
'input[name*="arrival"]',
'[data-testid="checkin-input"]',
'.date-picker input',
'.arrival-date input'
];
let datePicker = null;
for (const selector of datePickerSelectors) {
datePicker = await page.$(selector);
if (datePicker) {
console.log(`Found date picker: ${selector}`);
break;
}
}
if (datePicker) {
console.log('Clicking date picker...');
await datePicker.click();
await page.waitForTimeout(1000);
const screenshot3 = path.join(SCREENSHOT_DIR, 'eurocamp-03-date-picker-open.png');
await page.screenshot({ path: screenshot3, fullPage: true });
results.screenshots.push(screenshot3);
console.log('✓ Screenshot 3: Date picker open saved');
// Try to navigate to July 2026
// Most date pickers have month navigation
const nextMonthButtons = await page.$$('button:has-text(">"), .next-month, [aria-label*="next"], .calendar-nav-next');
for (let i = 0; i < 18; i++) { // Up to 18 months ahead
const monthYear = await page.$('.current-month, .calendar-month, [class*="month-year"]');
if (monthYear) {
const monthText = await monthYear.textContent();
if (monthText && monthText.includes('July 2026')) {
console.log('Found July 2026');
break;
}
}
for (const btn of nextMonthButtons) {
try {
await btn.click();
await page.waitForTimeout(200);
} catch (e) {}
}
}
// Select July 18
const day18 = await page.$('text="18":near(.calendar, :text("July")), td:has-text("18"), button:has-text("18")');
if (day18) {
await day18.click();
await page.waitForTimeout(500);
}
// Select August 2
const day2 = await page.$('text="2":near(.calendar, :text("August")), td:has-text("2"), button:has-text("2")');
if (day2) {
await day2.click();
await page.waitForTimeout(500);
}
const screenshot4 = path.join(SCREENSHOT_DIR, 'eurocamp-04-dates-selected.png');
await page.screenshot({ path: screenshot4, fullPage: true });
results.screenshots.push(screenshot4);
console.log('✓ Screenshot 4: Dates selected saved');
}
// Look for guest/party selector
console.log('\nStep 4: Looking for guest selector...');
const guestSelectors = [
'input[name*="adult"]',
'select[name*="adult"]',
'[data-testid="adults-input"]',
'.guest-selector',
'button:has-text("2 adults")',
'button:has-text("guests")'
];
for (const selector of guestSelectors) {
try {
const guestInput = await page.$(selector);
if (guestInput) {
console.log(`Found guest input: ${selector}`);
// Would need to interact based on element type
break;
}
} catch (e) {}
}
// Look for search/submit button
console.log('\nStep 5: Looking for search button...');
const searchButtonSelectors = [
'button[type="submit"]',
'button:has-text("Search")',
'button:has-text("Find prices")',
'button:has-text("Get quotes")',
'[data-testid="search-button"]'
];
for (const selector of searchButtonSelectors) {
try {
const searchBtn = await page.$(selector);
if (searchBtn) {
console.log(`Found search button: ${selector}`);
await searchBtn.click();
await page.waitForTimeout(5000);
const screenshot5 = path.join(SCREENSHOT_DIR, 'eurocamp-05-search-results.png');
await page.screenshot({ path: screenshot5, fullPage: true });
results.screenshots.push(screenshot5);
console.log('✓ Screenshot 5: Search results saved');
break;
}
} catch (e) {}
}
// Extract any visible prices
console.log('\nStep 6: Extracting prices from page...');
const bodyText = await page.textContent('body');
// Find price patterns
const ukPrices = bodyText.match(/£[\d,]+(?:\.\d{2})?/g) || [];
const euroPrices = bodyText.match(/€[\d,]+(?:\.\d{2})?/g) || [];
const totalPrices = bodyText.match(/total[^£€]*[£€][\d,]+/gi) || [];
results.rawPrices = { ukPrices: [...new Set(ukPrices)], euroPrices: [...new Set(euroPrices)], totalPrices };
console.log('Found UK prices:', [...new Set(ukPrices)].slice(0, 10));
console.log('Found Euro prices:', [...new Set(euroPrices)].slice(0, 10));
// Look for specific accommodation cards with prices
const priceCards = await page.$$('[class*="price"], [class*="accommodation"], [class*="card"]');
console.log(`Found ${priceCards.length} potential price cards`);
// Get the final URL
results.finalUrl = page.url();
console.log('Final URL:', results.finalUrl);
// Save page HTML for manual inspection
const html = await page.content();
const htmlPath = path.join(SCREENSHOT_DIR, 'eurocamp-page-content.html');
fs.writeFileSync(htmlPath, html);
results.notes.push(`HTML saved to ${htmlPath}`);
results.status = 'partial';
results.notes.push('Automated interaction completed. Some manual steps may be needed for complex booking forms.');
} catch (error) {
console.error('\n❌ Error:', error.message);
results.errors.push(error.message);
results.status = 'error';
try {
const errorScreenshot = path.join(SCREENSHOT_DIR, 'eurocamp-error-state.png');
await page.screenshot({ path: errorScreenshot, fullPage: true });
results.screenshots.push(errorScreenshot);
} catch (e) {
results.errors.push('Could not take error screenshot');
}
} finally {
await browser.close();
}
return results;
}
// Run
console.log('Starting Eurocamp price search...\n');
searchEurocamp()
.then(results => {
const outputPath = path.join(PRICES_DIR, 'eurocamp-domaine-des-ormes.json');
fs.writeFileSync(outputPath, JSON.stringify(results, null, 2));
console.log('\n========================================');
console.log('RESULTS');
console.log('========================================');
console.log('Status:', results.status);
console.log('Screenshots saved:', results.screenshots.length);
console.log('Prices found:', results.rawPrices?.ukPrices?.length || 0, 'UK,', results.rawPrices?.euroPrices?.length || 0, 'Euro');
console.log('Output file:', outputPath);
console.log('========================================\n');
})
.catch(err => {
console.error('Fatal error:', err);
process.exit(1);
});