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
This commit is contained in:
288
eurocamp-search.js
Normal file
288
eurocamp-search.js
Normal file
@@ -0,0 +1,288 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user