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 safeClick(page, selector, description) { try { const el = page.locator(selector).first(); if (await el.isVisible({ timeout: 3000 })) { await el.click(); console.log(`Clicked: ${description}`); await sleep(500); return true; } } catch (e) { console.log(`Could not click ${description}: ${e.message}`); } return false; } async function main() { console.log('Starting Brittany Ferries price search v2...'); console.log('Date: 18 July 2026 - 2 August 2026, Plymouth-Roscoff, 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' }); 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 Brittany Ferries ferry booking page console.log('\n=== Step 1: Navigate to booking page ==='); await page.goto('https://www.brittany-ferries.co.uk/ferry', { waitUntil: 'networkidle', timeout: 60000 }); await sleep(3000); await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v2-01-ferry-page.png'), fullPage: false }); results.screenshots.push('v2-01-ferry-page.png'); results.steps.push('Loaded ferry booking page'); // Accept cookies console.log('\n=== Step 2: Accept cookies ==='); try { const acceptBtn = page.locator('#onetrust-accept-btn-handler').first(); if (await acceptBtn.isVisible({ timeout: 3000 })) { await acceptBtn.click(); console.log('Accepted cookies'); await sleep(1000); results.steps.push('Accepted cookies'); } } catch (e) { console.log('No cookie banner visible'); } await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v2-02-after-cookies.png'), fullPage: false }); results.screenshots.push('v2-02-after-cookies.png'); // Step 3: Select route - Plymouth to Roscoff console.log('\n=== Step 3: Select route ==='); // Look for route selection dropdowns - they might be custom Angular components const routeSelectors = await page.locator('[class*="route"], [class*="departure"], [class*="arrival"]').all(); console.log(`Found ${routeSelectors.length} route-related elements`); // Try to find and click the departure port dropdown const departureSelectors = [ 'mat-form-field:has-text("From")', 'mat-form-field:has-text("Departure")', '[data-cy="departure-port"]', '.departure-port-selector', 'mat-select:has-text("Select")' ]; let departureClicked = false; for (const selector of departureSelectors) { try { const el = page.locator(selector).first(); if (await el.isVisible({ timeout: 2000 })) { await el.click(); console.log(`Clicked departure selector: ${selector}`); departureClicked = true; await sleep(1000); break; } } catch (e) {} } // Try clicking on mat-select elements if (!departureClicked) { const matSelects = await page.locator('mat-select').all(); console.log(`Found ${matSelects.length} mat-select elements`); // First mat-select is likely departure port if (matSelects.length > 0) { await matSelects[0].click(); console.log('Clicked first mat-select (departure port)'); await sleep(1000); await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v2-03-departure-dropdown.png'), fullPage: false }); results.screenshots.push('v2-03-departure-dropdown.png'); // Look for Plymouth option const plymouthOptions = [ 'mat-option:has-text("Plymouth")', '.mat-option:has-text("Plymouth")', 'span:has-text("Plymouth")' ]; for (const opt of plymouthOptions) { try { const optEl = page.locator(opt).first(); if (await optEl.isVisible({ timeout: 2000 })) { await optEl.click(); console.log('Selected Plymouth'); results.steps.push('Selected Plymouth as departure'); break; } } catch (e) {} } await sleep(500); } } await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v2-04-after-plymouth.png'), fullPage: false }); results.screenshots.push('v2-04-after-plymouth.png'); // Select arrival port - Roscoff const matSelects2 = await page.locator('mat-select').all(); if (matSelects2.length > 1) { await matSelects2[1].click(); console.log('Clicked second mat-select (arrival port)'); await sleep(1000); await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v2-05-arrival-dropdown.png'), fullPage: false }); results.screenshots.push('v2-05-arrival-dropdown.png'); // Look for Roscoff option const roscoffOptions = [ 'mat-option:has-text("Roscoff")', '.mat-option:has-text("Roscoff")', 'span:has-text("Roscoff")' ]; for (const opt of roscoffOptions) { try { const optEl = page.locator(opt).first(); if (await optEl.isVisible({ timeout: 2000 })) { await optEl.click(); console.log('Selected Roscoff'); results.steps.push('Selected Roscoff as arrival'); break; } } catch (e) {} } await sleep(500); } await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v2-06-after-roscoff.png'), fullPage: false }); results.screenshots.push('v2-06-after-roscoff.png'); // Step 4: Enter dates console.log('\n=== Step 4: Enter dates ==='); // Look for date inputs const dateInputs = await page.locator('input[placeholder*="date"], input[type="text"]').all(); console.log(`Found ${dateInputs.length} text inputs`); // Find outbound date input const outboundInput = page.locator('input[placeholder*="Outbound"]').first(); if (await outboundInput.isVisible({ timeout: 2000 })) { await outboundInput.click(); await sleep(500); await outboundInput.fill('18/07/2026'); console.log('Entered outbound date: 18/07/2026'); await page.keyboard.press('Enter'); await sleep(500); results.steps.push('Entered outbound date: 18/07/2026'); } // Find inbound date input const inboundInput = page.locator('input[placeholder*="Inbound"], input[placeholder*="Return"]').first(); if (await inboundInput.isVisible({ timeout: 2000 })) { await inboundInput.click(); await sleep(500); await inboundInput.fill('02/08/2026'); console.log('Entered inbound date: 02/08/2026'); await page.keyboard.press('Enter'); await sleep(500); results.steps.push('Entered inbound date: 02/08/2026'); } await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v2-07-dates-entered.png'), fullPage: false }); results.screenshots.push('v2-07-dates-entered.png'); // Step 5: Configure passengers and vehicle console.log('\n=== Step 5: Configure passengers and vehicle ==='); // Look for passenger selector const passengerSelectors = await page.locator('[class*="passenger"], mat-select, [data-cy*="passenger"]').all(); console.log(`Found ${passengerSelectors.length} potential passenger selectors`); // Try to find and interact with passenger dropdowns // Usually there's a dropdown for adults, children, and vehicles // Check if there are specific passenger count controls const adultPlus = page.locator('[class*="adult"] button:has-text("+"), [data-cy="adult-plus"]').first(); const adultMinus = page.locator('[class*="adult"] button:has-text("-"), [data-cy="adult-minus"]').first(); // We need 2 adults - check current state and adjust // This is tricky without seeing the actual UI // Look for any dropdown that might be for passengers const allSelects = await page.locator('mat-select').all(); console.log(`Total mat-select elements: ${allSelects.length}`); await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v2-08-before-passengers.png'), fullPage: false }); results.screenshots.push('v2-08-before-passengers.png'); // Step 6: Submit search console.log('\n=== Step 6: Submit search ==='); const searchButtons = [ 'button:has-text("Search")', 'button:has-text("Get quotes")', 'button:has-text("Find ferries")', 'button[type="submit"]', '.search-button', '[data-cy="search-button"]' ]; for (const btn of searchButtons) { try { const searchBtn = page.locator(btn).first(); if (await searchBtn.isVisible({ timeout: 1000 })) { await searchBtn.click(); console.log(`Clicked search button: ${btn}`); results.steps.push('Clicked search button'); break; } } catch (e) {} } // Wait for results console.log('Waiting for results...'); await sleep(5000); await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v2-09-results-page.png'), fullPage: true }); results.screenshots.push('v2-09-results-page.png'); // Step 7: Extract pricing information console.log('\n=== Step 7: Extract pricing ==='); const pageContent = await page.content(); fs.writeFileSync(path.join(SCREENSHOT_DIR, 'v2-results-page.html'), pageContent); const pageText = await page.evaluate(() => document.body.innerText); fs.writeFileSync(path.join(SCREENSHOT_DIR, 'v2-results-page.txt'), pageText); // Try to extract price information const pricePatterns = [ /£\d+/g, /\d+\.\d{2}/g, /total.*?£?(\d+)/gi ]; const prices = []; for (const pattern of pricePatterns) { const matches = pageText.match(pattern); if (matches) { prices.push(...matches); } } console.log('Found potential prices:', prices.slice(0, 20)); results.extractedPrices = [...new Set(prices)].slice(0, 20); // Try to find specific price elements const priceElements = await page.locator('[class*="price"], [data-cy*="price"]').all(); const elementPrices = []; for (const el of priceElements.slice(0, 10)) { try { const text = await el.textContent(); elementPrices.push(text); } catch (e) {} } console.log('Price elements found:', elementPrices); results.priceElements = elementPrices; results.url = page.url(); results.status = 'completed'; results.finalUrl = page.url(); } catch (error) { console.error('Error during scraping:', error); results.status = 'error'; results.error = error.message; results.stack = error.stack; await page.screenshot({ path: path.join(SCREENSHOT_DIR, 'v2-error.png'), fullPage: true }); results.screenshots.push('v2-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(`\n=== Results saved to: ${outputPath} ===`); return results; } main().catch(console.error);