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:
354
brittany-search-v3.js
Normal file
354
brittany-search-v3.js
Normal file
@@ -0,0 +1,354 @@
|
||||
const { chromium } = require('playwright');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const SCREENSHOT_DIR = path.join(process.env.HOME, 'holiday-planning', 'price-evidence');
|
||||
const OUTPUT_DIR = path.join(process.env.HOME, 'holiday-planning', 'prices');
|
||||
|
||||
async function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Starting Brittany Ferries price search v3...');
|
||||
console.log('Target: 18 July 2026 - 2 August 2026, Plymouth-Roscoff');
|
||||
console.log('Passengers: 2 adults, 1 child (6), 1 car');
|
||||
|
||||
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',
|
||||
locale: 'en-GB'
|
||||
});
|
||||
|
||||
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',
|
||||
steps: []
|
||||
};
|
||||
|
||||
try {
|
||||
// Step 1: Navigate to main Brittany Ferries site
|
||||
console.log('\n=== Step 1: Navigate to Brittany Ferries ===');
|
||||
await page.goto('https://www.brittany-ferries.co.uk', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
// Wait for the page to load
|
||||
await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
|
||||
await sleep(3000);
|
||||
|
||||
console.log('Current URL:', page.url());
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v3-01-homepage.png'), fullPage: false });
|
||||
results.screenshots.push('v3-01-homepage.png');
|
||||
results.steps.push('Loaded homepage: ' + page.url());
|
||||
|
||||
// Step 2: Accept cookies
|
||||
console.log('\n=== Step 2: Accept cookies ===');
|
||||
try {
|
||||
const acceptBtn = page.locator('#onetrust-accept-btn-handler');
|
||||
if (await acceptBtn.isVisible({ timeout: 5000 })) {
|
||||
await acceptBtn.click();
|
||||
console.log('Accepted cookies');
|
||||
await sleep(1000);
|
||||
results.steps.push('Accepted cookies');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('No cookie banner');
|
||||
}
|
||||
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v3-02-no-cookies.png'), fullPage: false });
|
||||
results.screenshots.push('v3-02-no-cookies.png');
|
||||
|
||||
// Step 3: Analyze the page structure
|
||||
console.log('\n=== Step 3: Analyze page structure ===');
|
||||
|
||||
// Get all interactive elements
|
||||
const pageStructure = await page.evaluate(() => {
|
||||
const elements = [];
|
||||
|
||||
// Find all buttons
|
||||
document.querySelectorAll('button, [role="button"], a.btn').forEach(el => {
|
||||
elements.push({
|
||||
type: 'button',
|
||||
text: el.textContent?.trim().slice(0, 50),
|
||||
class: el.className?.slice(0, 50),
|
||||
id: el.id
|
||||
});
|
||||
});
|
||||
|
||||
// Find all selects
|
||||
document.querySelectorAll('select, mat-select, [role="listbox"]').forEach(el => {
|
||||
elements.push({
|
||||
type: 'select',
|
||||
class: el.className?.slice(0, 50),
|
||||
id: el.id,
|
||||
placeholder: el.placeholder
|
||||
});
|
||||
});
|
||||
|
||||
// Find all inputs
|
||||
document.querySelectorAll('input').forEach(el => {
|
||||
elements.push({
|
||||
type: 'input',
|
||||
inputType: el.type,
|
||||
placeholder: el.placeholder,
|
||||
name: el.name,
|
||||
id: el.id
|
||||
});
|
||||
});
|
||||
|
||||
return elements;
|
||||
});
|
||||
|
||||
console.log('Page structure found:');
|
||||
console.log(JSON.stringify(pageStructure, null, 2));
|
||||
results.pageStructure = pageStructure;
|
||||
|
||||
// Step 4: Find and interact with booking form
|
||||
console.log('\n=== Step 4: Interact with booking form ===');
|
||||
|
||||
// Try to find route selection
|
||||
// Look for dropdowns that might contain route info
|
||||
|
||||
// Method 1: Look for specific route selectors
|
||||
const routeDropdowns = await page.locator('[class*="route"], [class*="port"], [class*="destination"]').all();
|
||||
console.log(`Found ${routeDropdowns.length} route/port elements`);
|
||||
|
||||
// Method 2: Look for mat-form-field (Angular Material)
|
||||
const formFields = await page.locator('mat-form-field').all();
|
||||
console.log(`Found ${formFields.length} mat-form-field elements`);
|
||||
|
||||
// Method 3: Look for any dropdown triggers
|
||||
const dropdowns = await page.locator('.dropdown, [data-toggle="dropdown"], mat-select-trigger').all();
|
||||
console.log(`Found ${dropdowns.length} dropdown elements`);
|
||||
|
||||
// Take a screenshot before interaction
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v3-03-before-interaction.png'), fullPage: true });
|
||||
results.screenshots.push('v3-03-before-interaction.png');
|
||||
|
||||
// Try clicking on the Ferry tab if it exists
|
||||
const ferryTab = page.locator('button:has-text("Ferry"), a:has-text("Ferry"), [class*="tab"]:has-text("Ferry")').first();
|
||||
if (await ferryTab.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
await ferryTab.click();
|
||||
console.log('Clicked Ferry tab');
|
||||
await sleep(1000);
|
||||
results.steps.push('Clicked Ferry tab');
|
||||
}
|
||||
|
||||
// Try to find and click on the first mat-select or dropdown
|
||||
const matSelects = await page.locator('mat-select').all();
|
||||
console.log(`Found ${matSelects.length} mat-select elements`);
|
||||
|
||||
if (matSelects.length > 0) {
|
||||
// Click first mat-select (likely departure port)
|
||||
await matSelects[0].scrollIntoViewIfNeeded();
|
||||
await matSelects[0].click();
|
||||
console.log('Clicked first mat-select');
|
||||
await sleep(1500);
|
||||
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v3-04-dropdown-open.png'), fullPage: false });
|
||||
results.screenshots.push('v3-04-dropdown-open.png');
|
||||
|
||||
// Try to find Plymouth
|
||||
const options = await page.locator('mat-option').all();
|
||||
console.log(`Found ${options.length} options`);
|
||||
|
||||
for (const opt of options) {
|
||||
const text = await opt.textContent();
|
||||
if (text && text.toLowerCase().includes('plymouth')) {
|
||||
await opt.click();
|
||||
console.log('Selected Plymouth');
|
||||
results.steps.push('Selected Plymouth');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await sleep(500);
|
||||
|
||||
// Click second mat-select (likely arrival port)
|
||||
if (matSelects.length > 1) {
|
||||
await matSelects[1].click();
|
||||
console.log('Clicked second mat-select');
|
||||
await sleep(1500);
|
||||
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v3-05-arrival-dropdown.png'), fullPage: false });
|
||||
results.screenshots.push('v3-05-arrival-dropdown.png');
|
||||
|
||||
const arrivalOptions = await page.locator('mat-option').all();
|
||||
for (const opt of arrivalOptions) {
|
||||
const text = await opt.textContent();
|
||||
if (text && text.toLowerCase().includes('roscoff')) {
|
||||
await opt.click();
|
||||
console.log('Selected Roscoff');
|
||||
results.steps.push('Selected Roscoff');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v3-06-after-routes.png'), fullPage: false });
|
||||
results.screenshots.push('v3-06-after-routes.png');
|
||||
|
||||
// Step 5: Enter dates
|
||||
console.log('\n=== Step 5: Enter dates ===');
|
||||
|
||||
// Look for date inputs
|
||||
const dateInputSelectors = [
|
||||
'input[placeholder*="date" i]',
|
||||
'input[placeholder*="outbound" i]',
|
||||
'input[placeholder*="inbound" i]',
|
||||
'input[placeholder*="return" i]',
|
||||
'input[formcontrolname*="date" i]',
|
||||
'input[matinput]'
|
||||
];
|
||||
|
||||
for (const selector of dateInputSelectors) {
|
||||
const inputs = await page.locator(selector).all();
|
||||
for (const input of inputs) {
|
||||
const placeholder = await input.getAttribute('placeholder').catch(() => '');
|
||||
console.log(`Found input: ${selector}, placeholder: ${placeholder}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to enter dates directly into inputs
|
||||
const outboundDateInput = page.locator('input').filter({ hasText: '' }).nth(0);
|
||||
const allInputs = await page.locator('input[type="text"], input:not([type])').all();
|
||||
console.log(`Found ${allInputs.length} text inputs`);
|
||||
|
||||
// Look for date picker triggers
|
||||
const datePickerTriggers = await page.locator('[class*="datepicker"], [class*="calendar"], mat-datepicker-toggle').all();
|
||||
console.log(`Found ${datePickerTriggers.length} date picker elements`);
|
||||
|
||||
// Try clicking on date input and entering date
|
||||
for (let i = 0; i < Math.min(2, allInputs.length); i++) {
|
||||
const input = allInputs[i];
|
||||
const placeholder = await input.getAttribute('placeholder').catch(() => '');
|
||||
const name = await input.getAttribute('name').catch(() => '');
|
||||
console.log(`Input ${i}: placeholder="${placeholder}", name="${name}"`);
|
||||
|
||||
if (placeholder?.toLowerCase().includes('outbound') || name?.includes('outbound')) {
|
||||
await input.click();
|
||||
await input.fill('18/07/2026');
|
||||
console.log('Entered outbound date');
|
||||
results.steps.push('Entered outbound date: 18/07/2026');
|
||||
await page.keyboard.press('Enter');
|
||||
await sleep(500);
|
||||
} else if (placeholder?.toLowerCase().includes('inbound') || placeholder?.toLowerCase().includes('return') || name?.includes('inbound')) {
|
||||
await input.click();
|
||||
await input.fill('02/08/2026');
|
||||
console.log('Entered inbound date');
|
||||
results.steps.push('Entered inbound date: 02/08/2026');
|
||||
await page.keyboard.press('Enter');
|
||||
await sleep(500);
|
||||
}
|
||||
}
|
||||
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v3-07-dates.png'), fullPage: false });
|
||||
results.screenshots.push('v3-07-dates.png');
|
||||
|
||||
// Step 6: Configure passengers
|
||||
console.log('\n=== Step 6: Configure passengers ===');
|
||||
|
||||
// Look for passenger/vehicle selectors
|
||||
// These might be mat-selects with labels like "Passengers", "Adults", "Vehicle"
|
||||
|
||||
const remainingSelects = await page.locator('mat-select').all();
|
||||
console.log(`Remaining mat-selects: ${remainingSelects.length}`);
|
||||
|
||||
for (let i = 0; i < remainingSelects.length; i++) {
|
||||
const select = remainingSelects[i];
|
||||
const parentText = await select.locator('xpath=..').textContent().catch(() => '');
|
||||
console.log(`Select ${i}: parent text = "${parentText?.slice(0, 50)}"`);
|
||||
|
||||
if (parentText?.toLowerCase().includes('adult') || parentText?.toLowerCase().includes('passenger')) {
|
||||
await select.click();
|
||||
await sleep(500);
|
||||
|
||||
const opts = await page.locator('mat-option').all();
|
||||
for (const opt of opts) {
|
||||
const text = await opt.textContent();
|
||||
if (text?.includes('2')) {
|
||||
await opt.click();
|
||||
console.log('Selected 2 adults');
|
||||
results.steps.push('Selected 2 adults');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v3-08-passengers.png'), fullPage: false });
|
||||
results.screenshots.push('v3-08-passengers.png');
|
||||
|
||||
// Step 7: Submit form
|
||||
console.log('\n=== Step 7: Submit search ===');
|
||||
|
||||
const searchBtn = page.locator('button:has-text("Search"), button:has-text("Get quote"), button[type="submit"]').first();
|
||||
if (await searchBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
await searchBtn.click();
|
||||
console.log('Clicked search button');
|
||||
results.steps.push('Clicked search');
|
||||
|
||||
// Wait for results
|
||||
console.log('Waiting for results to load...');
|
||||
await sleep(5000);
|
||||
await page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {});
|
||||
}
|
||||
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v3-09-results.png'), fullPage: true });
|
||||
results.screenshots.push('v3-09-results.png');
|
||||
|
||||
// Step 8: Extract prices
|
||||
console.log('\n=== Step 8: Extract pricing ===');
|
||||
|
||||
const pageText = await page.evaluate(() => document.body.innerText);
|
||||
fs.writeFileSync(path.join(SCREENSHOT_DIR, 'v3-page-text.txt'), pageText);
|
||||
|
||||
const pageHtml = await page.content();
|
||||
fs.writeFileSync(path.join(SCREENSHOT_DIR, 'v3-page.html'), pageHtml);
|
||||
|
||||
// Look for prices
|
||||
const priceMatch = pageText.match(/£[\d,]+(?:\.\d{2})?/g);
|
||||
console.log('Found prices:', priceMatch);
|
||||
|
||||
results.url = page.url();
|
||||
results.extractedPrices = priceMatch ? [...new Set(priceMatch)] : [];
|
||||
results.status = 'completed';
|
||||
|
||||
console.log('\nFinal URL:', page.url());
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
results.status = 'error';
|
||||
results.error = error.message;
|
||||
|
||||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v3-error.png'), fullPage: true });
|
||||
results.screenshots.push('v3-error.png');
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
|
||||
// Save results
|
||||
const outputPath = path.join(OUTPUT_DIR, '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