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:
249
brittany-search.js
Normal file
249
brittany-search.js
Normal file
@@ -0,0 +1,249 @@
|
||||
const { chromium } = require('playwright');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const SCREENSHOT_DIR = path.join(process.env.HOME, 'holiday-planning', 'price-evidence');
|
||||
|
||||
async function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Starting Brittany Ferries price search...');
|
||||
|
||||
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 (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 results = {
|
||||
searchDate: new Date().toISOString(),
|
||||
route: 'Plymouth to Roscoff',
|
||||
outboundDate: '2026-07-18',
|
||||
returnDate: '2026-08-02',
|
||||
passengers: { adults: 2, children: 1, childAges: [6] },
|
||||
vehicle: '1 car',
|
||||
screenshots: [],
|
||||
status: 'in_progress'
|
||||
};
|
||||
|
||||
try {
|
||||
// Step 1: Navigate to Brittany Ferries
|
||||
console.log('Navigating to Brittany Ferries...');
|
||||
await page.goto('https://www.brittany-ferries.co.uk', { waitUntil: 'networkidle', timeout: 60000 });
|
||||
await sleep(2000);
|
||||
|
||||
// Take initial screenshot
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, '01-homepage.png'), fullPage: false });
|
||||
results.screenshots.push('01-homepage.png');
|
||||
console.log('Screenshot: homepage');
|
||||
|
||||
// Step 2: Accept cookies if prompted
|
||||
console.log('Checking for cookie banner...');
|
||||
try {
|
||||
const acceptCookies = await page.locator('button:has-text("Accept"), button:has-text("accept"), button:has-text("OK"), #onetrust-accept-btn-handler, button[id*="cookie"]').first();
|
||||
if (await acceptCookies.isVisible({ timeout: 3000 })) {
|
||||
await acceptCookies.click();
|
||||
console.log('Accepted cookies');
|
||||
await sleep(1000);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('No cookie banner found or already accepted');
|
||||
}
|
||||
|
||||
// Step 3: Look for and interact with booking form
|
||||
console.log('Looking for booking form...');
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, '02-before-form.png'), fullPage: false });
|
||||
results.screenshots.push('02-before-form.png');
|
||||
|
||||
// Try to find route selector
|
||||
const routeSelectors = [
|
||||
'select[name*="route"]',
|
||||
'select[id*="route"]',
|
||||
'[data-testid*="route"]',
|
||||
'.route-selector',
|
||||
'#route'
|
||||
];
|
||||
|
||||
let routeSelect = null;
|
||||
for (const selector of routeSelectors) {
|
||||
try {
|
||||
const el = page.locator(selector).first();
|
||||
if (await el.isVisible({ timeout: 1000 })) {
|
||||
routeSelect = el;
|
||||
console.log(`Found route selector: ${selector}`);
|
||||
break;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Look for booking form elements more broadly
|
||||
console.log('Searching for form elements...');
|
||||
|
||||
// Find outbound port
|
||||
const outboundSelectors = [
|
||||
'select[name*="departure"], select[id*="departure"]',
|
||||
'select[name*="from"], select[id*="from"]',
|
||||
'[data-testid*="departure-port"]',
|
||||
'#departure-port'
|
||||
];
|
||||
|
||||
// Try to find and click on booking form area
|
||||
const bookingFormSelectors = [
|
||||
'.booking-form',
|
||||
'#booking-form',
|
||||
'[data-testid="booking-form"]',
|
||||
'.ferry-search',
|
||||
'.search-form'
|
||||
];
|
||||
|
||||
for (const selector of bookingFormSelectors) {
|
||||
try {
|
||||
const form = page.locator(selector).first();
|
||||
if (await form.isVisible({ timeout: 1000 })) {
|
||||
console.log(`Found booking form: ${selector}`);
|
||||
break;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Try to find any dropdown/button to start booking
|
||||
console.log('Looking for booking initiation elements...');
|
||||
|
||||
// Common patterns for ferry booking forms
|
||||
const possibleStartButtons = [
|
||||
'button:has-text("Book")',
|
||||
'button:has-text("Search")',
|
||||
'button:has-text("Get a quote")',
|
||||
'a:has-text("Book now")',
|
||||
'.book-now',
|
||||
'#book-now'
|
||||
];
|
||||
|
||||
// Wait for page to be fully interactive
|
||||
await sleep(2000);
|
||||
|
||||
// Take a screenshot of current state
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, '03-form-state.png'), fullPage: true });
|
||||
results.screenshots.push('03-form-state.png');
|
||||
|
||||
// Get all form elements on page for debugging
|
||||
const formElements = await page.evaluate(() => {
|
||||
const inputs = Array.from(document.querySelectorAll('input, select, button'));
|
||||
return inputs.map(el => ({
|
||||
tag: el.tagName,
|
||||
type: el.type || el.tagName,
|
||||
name: el.name || el.id || el.className,
|
||||
placeholder: el.placeholder,
|
||||
value: el.value
|
||||
}));
|
||||
});
|
||||
console.log('Found form elements:', JSON.stringify(formElements, null, 2));
|
||||
|
||||
// Try to interact with the route/departure selection
|
||||
// Look for Plymouth specifically
|
||||
try {
|
||||
// Common ferry booking form patterns
|
||||
const routeSelect = page.locator('select').first();
|
||||
if (await routeSelect.isVisible({ timeout: 2000 })) {
|
||||
await routeSelect.click();
|
||||
await sleep(500);
|
||||
|
||||
// Look for Plymouth option
|
||||
const plymouthOption = page.locator('option:has-text("Plymouth"), li:has-text("Plymouth")').first();
|
||||
if (await plymouthOption.isVisible({ timeout: 1000 })) {
|
||||
await plymouthOption.click();
|
||||
console.log('Selected Plymouth');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Could not select route via standard select');
|
||||
}
|
||||
|
||||
// Try alternative - maybe it's a custom dropdown
|
||||
try {
|
||||
// Look for dropdown triggers
|
||||
const dropdownTriggers = await page.locator('[class*="dropdown"], [class*="select"], [role="combobox"]').all();
|
||||
console.log(`Found ${dropdownTriggers.length} dropdown elements`);
|
||||
|
||||
for (const trigger of dropdownTriggers.slice(0, 3)) {
|
||||
try {
|
||||
if (await trigger.isVisible()) {
|
||||
await trigger.click();
|
||||
await sleep(300);
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, '04-dropdown-open.png'), fullPage: false });
|
||||
results.screenshots.push('04-dropdown-open.png');
|
||||
break;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('No custom dropdowns found');
|
||||
}
|
||||
|
||||
// Take screenshot of current state
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, '05-current-state.png'), fullPage: true });
|
||||
results.screenshots.push('05-current-state.png');
|
||||
|
||||
// Get page HTML for analysis
|
||||
const html = await page.content();
|
||||
fs.writeFileSync(path.join(SCREENSHOT_DIR, 'page-source.html'), html);
|
||||
console.log('Saved page source');
|
||||
|
||||
// Get all visible text for analysis
|
||||
const visibleText = await page.evaluate(() => document.body.innerText);
|
||||
fs.writeFileSync(path.join(SCREENSHOT_DIR, 'page-text.txt'), visibleText);
|
||||
console.log('Saved page text');
|
||||
|
||||
// Check if we can find booking elements by looking at the page structure
|
||||
const pageAnalysis = await page.evaluate(() => {
|
||||
return {
|
||||
title: document.title,
|
||||
url: window.location.href,
|
||||
hasBookingForm: !!document.querySelector('form[action*="book"], form[action*="search"]'),
|
||||
buttons: Array.from(document.querySelectorAll('button')).map(b => b.innerText || b.textContent).slice(0, 10),
|
||||
links: Array.from(document.querySelectorAll('a')).filter(a => a.href.includes('book') || a.href.includes('quote')).map(a => ({ text: a.innerText, href: a.href })).slice(0, 10)
|
||||
};
|
||||
});
|
||||
console.log('Page analysis:', JSON.stringify(pageAnalysis, null, 2));
|
||||
|
||||
// If we found a booking link, try clicking it
|
||||
if (pageAnalysis.links.length > 0) {
|
||||
console.log('Found booking links, trying first one...');
|
||||
await page.goto(pageAnalysis.links[0].href, { waitUntil: 'networkidle', timeout: 60000 });
|
||||
await sleep(2000);
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, '06-booking-page.png'), fullPage: true });
|
||||
results.screenshots.push('06-booking-page.png');
|
||||
}
|
||||
|
||||
results.status = 'partial';
|
||||
results.pageAnalysis = pageAnalysis;
|
||||
results.url = page.url();
|
||||
results.note = 'Page loaded but complex booking form requires further interaction';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error during scraping:', error);
|
||||
results.status = 'error';
|
||||
results.error = error.message;
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'error-screenshot.png'), fullPage: true });
|
||||
results.screenshots.push('error-screenshot.png');
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
|
||||
// Save results
|
||||
const outputPath = path.join(process.env.HOME, 'holiday-planning', 'prices', 'brittany-ferries-plymouth-roscoff.json');
|
||||
fs.writeFileSync(outputPath, JSON.stringify(results, null, 2));
|
||||
console.log(`\nResults saved to: ${outputPath}`);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
Reference in New Issue
Block a user