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);