Files
holiday-plans/eurotunnel-scrape.mjs
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

288 lines
8.9 KiB
JavaScript

import { chromium } from 'playwright';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const screenshotDir = path.join(__dirname, 'price-evidence');
const formatDate = (date) => {
const d = new Date(date);
const day = String(d.getDate()).padStart(2, '0');
const month = String(d.getMonth() + 1).padStart(2, '0');
const year = d.getFullYear();
return `${day}/${month}/${year}`;
};
async function main() {
const browser = await chromium.launch({
headless: true,
slowMo: 500
});
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/120.0.0.0 Safari/537.36'
});
const page = await context.newPage();
const result = {
url: 'https://www.eurotunnel.com/uk/',
searchParams: {
route: 'Folkestone to Calais',
outbound: '18 July 2026',
return: '02 August 2026',
vehicle: '1 car (standard)'
},
price: null,
crossingTimes: [],
screenshots: [],
timestamp: new Date().toISOString(),
notes: []
};
try {
console.log('Navigating to Eurotunnel...');
await page.goto('https://www.eurotunnel.com/uk/', { waitUntil: 'networkidle' });
await page.screenshot({ path: path.join(screenshotDir, '01-homepage.png'), fullPage: true });
result.screenshots.push('01-homepage.png');
// Handle cookie consent if present
console.log('Checking for cookie banner...');
try {
const acceptCookies = page.locator('button:has-text("Accept"), button:has-text("accept"), button:has-text("Allow"), #onetrust-accept-btn-handler, button[id*="accept"]').first();
if (await acceptCookies.isVisible({ timeout: 5000 }).catch(() => false)) {
await acceptCookies.click();
console.log('Accepted cookies');
await page.waitForTimeout(1000);
}
} catch (e) {
console.log('No cookie banner found or already handled');
}
await page.screenshot({ path: path.join(screenshotDir, '02-after-cookies.png'), fullPage: true });
result.screenshots.push('02-after-cookies.png');
// Look for booking form - try multiple selectors
console.log('Looking for booking form...');
// Try to find the booking widget
const bookingSelectors = [
'#booking-widget',
'.booking-widget',
'[data-testid="booking-form"]',
'form[action*="booking"]',
'.leisure-booking',
'#leisure-booking'
];
let formFound = false;
for (const selector of bookingSelectors) {
try {
const form = page.locator(selector).first();
if (await form.isVisible({ timeout: 2000 }).catch(() => false)) {
console.log(`Found form with selector: ${selector}`);
formFound = true;
break;
}
} catch (e) {}
}
// Try to select route (Folkestone to Calais)
console.log('Selecting route...');
try {
// Look for route selector
const routeSelectors = [
'select[name*="route"]',
'[data-testid="route-selector"]',
'select[id*="departure"]',
'.route-selector select'
];
for (const sel of routeSelectors) {
try {
const routeSelect = page.locator(sel).first();
if (await routeSelect.isVisible({ timeout: 2000 }).catch(() => false)) {
await routeSelect.selectOption({ label: /Folkestone.*Calais|Calais.*Folkestone/i });
console.log('Selected route');
break;
}
} catch (e) {}
}
} catch (e) {
result.notes.push(`Route selection issue: ${e.message}`);
}
// Look for date inputs
console.log('Filling in dates...');
// Try to find and fill outbound date
const outboundDateSelectors = [
'input[name*="outbound"]',
'input[name*="departure"]',
'input[name*="outDate"]',
'[data-testid="outbound-date"]',
'#outbound-date',
'input[placeholder*="outbound"]',
'input[placeholder*="departure"]'
];
for (const sel of outboundDateSelectors) {
try {
const dateInput = page.locator(sel).first();
if (await dateInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await dateInput.click();
await page.waitForTimeout(500);
await dateInput.fill('18/07/2026');
console.log('Filled outbound date');
break;
}
} catch (e) {}
}
// Try to find and fill return date
const returnDateSelectors = [
'input[name*="return"]',
'input[name*="returnDate"]',
'[data-testid="return-date"]',
'#return-date',
'input[placeholder*="return"]'
];
for (const sel of returnDateSelectors) {
try {
const dateInput = page.locator(sel).first();
if (await dateInput.isVisible({ timeout: 2000 }).catch(() => false)) {
await dateInput.click();
await page.waitForTimeout(500);
await dateInput.fill('02/08/2026');
console.log('Filled return date');
break;
}
} catch (e) {}
}
await page.screenshot({ path: path.join(screenshotDir, '03-form-filled.png'), fullPage: true });
result.screenshots.push('03-form-filled.png');
// Look for search/submit button
console.log('Looking for search button...');
const searchButtonSelectors = [
'button[type="submit"]',
'button:has-text("Search")',
'button:has-text("Find")',
'button:has-text("Get prices")',
'button:has-text("Book")',
'.search-button',
'#search-button',
'[data-testid="search-button"]'
];
let searchClicked = false;
for (const sel of searchButtonSelectors) {
try {
const btn = page.locator(sel).first();
if (await btn.isVisible({ timeout: 2000 }).catch(() => false)) {
await btn.click();
console.log(`Clicked search button: ${sel}`);
searchClicked = true;
break;
}
} catch (e) {}
}
if (!searchClicked) {
// Try pressing Enter as fallback
await page.keyboard.press('Enter');
console.log('Pressed Enter as fallback');
}
// Wait for results page
console.log('Waiting for results...');
await page.waitForTimeout(3000);
// Wait for navigation or results to appear
try {
await page.waitForURL('**/booking/**', { timeout: 30000 });
} catch (e) {
console.log('URL didn\'t change to booking, checking current page...');
}
await page.screenshot({ path: path.join(screenshotDir, '04-results-page.png'), fullPage: true });
result.screenshots.push('04-results-page.png');
// Extract price
console.log('Extracting price...');
const priceSelectors = [
'.price',
'.total-price',
'[data-testid="price"]',
'.booking-price',
'span:has-text("£")'
];
for (const sel of priceSelectors) {
try {
const priceElements = await page.locator(sel).all();
for (const el of priceElements) {
const text = await el.textContent();
const priceMatch = text.match(/£[\d,]+\.?\d*/);
if (priceMatch) {
console.log(`Found price: ${priceMatch[0]}`);
if (!result.price || priceMatch[0].length > result.price.length) {
result.price = priceMatch[0];
}
}
}
} catch (e) {}
}
// Extract crossing times
console.log('Extracting crossing times...');
try {
const timeElements = await page.locator('time, .time, [data-testid*="time"], .departure-time, .arrival-time').all();
for (const el of timeElements) {
const text = await el.textContent();
if (text && text.trim()) {
result.crossingTimes.push(text.trim());
}
}
} catch (e) {
result.notes.push(`Time extraction issue: ${e.message}`);
}
// Get final URL
result.url = page.url();
await page.screenshot({ path: path.join(screenshotDir, '05-final-state.png'), fullPage: true });
result.screenshots.push('05-final-state.png');
// Try to get page content for analysis
const pageContent = await page.content();
fs.writeFileSync(path.join(screenshotDir, 'page-content.html'), pageContent);
} catch (error) {
result.error = error.message;
result.notes.push(`Error: ${error.message}`);
console.error('Error:', error);
await page.screenshot({ path: path.join(screenshotDir, 'error-screenshot.png'), fullPage: true });
result.screenshots.push('error-screenshot.png');
}
await browser.close();
// Save results
fs.writeFileSync(
path.join(__dirname, 'prices', 'eurotunnel.json'),
JSON.stringify(result, null, 2)
);
console.log('\n=== Results ===');
console.log(JSON.stringify(result, null, 2));
return result;
}
main().catch(console.error);