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
288 lines
8.9 KiB
JavaScript
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);
|