import puppeteer, { HTTPResponse } from 'puppeteer'; import axios from 'axios'; import * as cheerio from 'cheerio'; const targetUrl = process.argv[2] || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'; const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'klz2026'; // Utility for hardcoded delays const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); async function main() { console.log(`\n๐Ÿš€ Starting E2E Form Submission Check for: ${targetUrl}`); // 0. Pre-warming settlement: Wait for deployment to respond (sync containers/Varnish) console.log(`โณ Pre-warming: Waiting for deployment to return 200 OK...`); const maxWaitMs = 60000; const startTime = Date.now(); let warmed = false; while (Date.now() - startTime < maxWaitMs) { try { const resp = await axios.get(targetUrl, { headers: { Cookie: `klz_gatekeeper_session=${gatekeeperPassword}` }, timeout: 5000, validateStatus: (s) => s === 200, }); if (resp.status === 200) { warmed = true; break; } } catch (e) { // Quietly retry } await delay(2000); } if (!warmed) { console.warn(`โš ๏ธ Pre-warming timed out after 60s. Proceeding anyway...`); } else { console.log(`โœ… Deployment settled and responding.`); } // Brief stabilization buffer await delay(5000); // 1. Fetch Sitemap to discover the contact page and a product page const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`; let urls: string[] = []; try { console.log(`๐Ÿ“ฅ Fetching sitemap from ${sitemapUrl}...`); const response = await axios.get(sitemapUrl, { headers: { Cookie: `klz_gatekeeper_session=${gatekeeperPassword}` }, }); const $ = cheerio.load(response.data, { xmlMode: true }); urls = $('url loc') .map((i, el) => $(el).text()) .get(); // Normalize to target URL instance const urlPattern = /https?:\/\/[^\/]+/; urls = [...new Set(urls)] .filter((u) => u.startsWith('http')) .map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, ''))) .sort(); } catch (err: any) { console.error(`โŒ Failed to fetch sitemap: ${err.message}`); process.exit(1); } const contactUrl = urls.find((u) => u.includes('/de/kontakt')); // Ensure we select an actual product page (depth >= 7: http://host/de/produkte/category/product) const productUrl = urls.find( (u) => u.includes('/de/produkte/') && new URL(u).pathname.split('/').filter(Boolean).length >= 4, ); if (!contactUrl) { console.error(`โŒ Could not find contact page in sitemap. Ensure /de/kontakt exists.`); process.exit(1); } if (!productUrl) { console.error( `โŒ Could not find a product page in sitemap. Form testing requires at least one product page.`, ); process.exit(1); } console.log(`โœ… Discovered Contact Page: ${contactUrl}`); console.log(`โœ… Discovered Product Page: ${productUrl}`); // 2. Launch Headless Browser console.log(`\n๐Ÿ•ท๏ธ Launching Puppeteer Headless Engine...`); const browser = await puppeteer.launch({ headless: true, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--ignore-certificate-errors', '--disable-web-security', '--disable-features=IsolateOrigins,site-per-process', ], }); const page = await browser.newPage(); // Enable Request Interception to force cache-busting on ALL static assets await page.setRequestInterception(true); page.on('request', (request) => { const url = request.url(); // Intercept Next.js chunks and data to bypass Varnish 404-caching // Includes standard /_next/ and subpath /gatekeeper/_next/ if ( (url.includes('/_next/static/') || url.includes('/_next/data/') || url.includes('/gatekeeper/_next/')) && !url.includes('?cb=') && !url.includes('&cb=') ) { const buster = `cb=${Date.now()}`; const newUrl = url.includes('?') ? `${url}&${buster}` : `${url}?${buster}`; request.continue({ url: newUrl }); } else { request.continue(); } }); let chunkErrorsDetected = false; page.on('console', (msg) => { const text = msg.text(); console.log('๐Ÿ’ป BROWSER CONSOLE:', text); if (text.includes('ChunkLoadError') || text.includes('failed to load chunk')) { chunkErrorsDetected = true; } }); page.on('pageerror', (error) => { console.error('๐Ÿ’ป BROWSER ERROR:', error.message); if (error.message.includes('ChunkLoadError')) { chunkErrorsDetected = true; } }); page.on('requestfailed', (request) => { const url = request.url(); const failure = request.failure()?.errorText; console.error('๐Ÿ’ป BROWSER REQUEST FAILED:', url, failure); if (url.endsWith('.js') && (failure === 'net::ERR_ABORTED' || failure?.includes('404'))) { chunkErrorsDetected = true; } }); // Helper for resilient navigation with cache-busting fallback const navigateWithRetry = async (url: string, label: string) => { chunkErrorsDetected = false; console.log(`\n๐Ÿงช Testing ${label} on: ${url}`); // First attempt: Wait for network to be relatively idle await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 }); // REDIRECTION CHECK: Logging redirected landing page const finalUrl = page.url(); if (finalUrl !== url && !finalUrl.includes(url)) { console.log(` ๐Ÿ”€ Redirected to: ${finalUrl}`); } if (chunkErrorsDetected) { const buster = `cb=${Date.now()}`; const cbUrl = finalUrl.includes('?') ? `${finalUrl}&${buster}` : `${finalUrl}?${buster}`; console.warn( ` โš ๏ธ Assets failed to load (Varnish staleness suspected). Retrying with cache-buster: ${cbUrl}`, ); chunkErrorsDetected = false; await page.goto(cbUrl, { waitUntil: 'networkidle2', timeout: 30000 }); } // Capture HTML on failure during hydration wait try { await page.waitForNetworkIdle({ idleTime: 1000, timeout: 5000 }).catch(() => {}); } catch (e) { console.warn(' โš ๏ธ Network idle timeout, proceeding with current state.'); } }; // 3. Authenticate through Gatekeeper login form try { // Navigate to a protected page so Gatekeeper redirects us to the login screen // RELAXED NAVIGATION: Use domcontentloaded to handle rapid redirects await page.goto(contactUrl, { waitUntil: 'domcontentloaded', timeout: 60000 }); // BOUNCE BUFFER: Wait for potential Gatekeeper redirect chain to settle console.log(` Waiting for potential Gatekeeper redirect bounce...`); await delay(3000); // Check if we landed on the Gatekeeper login page const isGatekeeperPage = await page.$('input[name="password"]'); if (isGatekeeperPage) { console.log(` Gatekeeper gate detected. Logging in...`); await page.type('input[name="password"]', gatekeeperPassword); await Promise.all([ page.waitForNavigation({ waitUntil: 'domcontentloaded' }), page.click('button[type="submit"]'), ]); // LOGIN SYNC BUFFER: Let the authenticated session settle console.log(` Authenticating session...`); await delay(3000); console.log(`โœ… Gatekeeper authentication successful!`); } else { console.log(`โœ… Already authenticated (no Gatekeeper gate detected).`); } } catch (err: any) { console.error(`โŒ Gatekeeper authentication failed: ${err.message}`); await browser.close(); process.exit(1); } let hasErrors = false; // 4. Test Contact Form try { await navigateWithRetry(contactUrl, 'Contact Form'); // Ensure React has hydrated completely - CI runners can be slow console.log(` โณ Waiting for hydration (5s)...`); await delay(5000); // Ensure form is visible and interactive try { // Find the form input by name await page.waitForSelector('input[name="name"]', { visible: true, timeout: 15000 }); } catch (e) { console.error('โŒ Failed to find Contact Form input. Page Title:', await page.title()); const html = await page.content(); console.log(`๐Ÿ’ป FULL HTML DUMP (top 2000 chars):\n${html.slice(0, 2000)}`); throw e; } // Wait specifically for hydration logic to initialize the onSubmit handler await page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 2000))); // Fill form fields await page.type('input[name="name"]', 'Automated E2E Test'); await page.type('input[name="email"]', 'testing@mintel.me'); await page.type( 'textarea[name="message"]', 'This is an automated test verifying the contact form submission.', ); // Give state a moment to settle await page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 500))); console.log(` โณ Waiting for alert selector (30s)...`); try { // Explicitly click submit and wait for navigation/state-change await Promise.all([ page.waitForSelector('[role="alert"]', { timeout: 30000 }), page.click('button[type="submit"]'), ]); } catch (e) { console.error(`โŒ Timeout waiting for [role="alert"]. Dumping page content:`); const bodyHTML = await page.evaluate(() => document.body.innerHTML.slice(0, 1000)); console.log(`๐Ÿ’ป PAGE BODY (partial): ${bodyHTML}`); throw e; } const alertText = await page.$eval('[role="alert"]', (el) => el.textContent); console.log(` ๐Ÿ”” Alert text: ${alertText}`); // Detection robust for both English and German versions const isError = alertText?.toLowerCase().includes('failed') || alertText?.toLowerCase().includes('wrong') || alertText?.toLowerCase().includes('fehlgeschlagen') || alertText?.toLowerCase().includes('schief gelaufen') || alertText?.toLowerCase().includes('fehler'); if (isError) { throw new Error(`Form submitted but showed error: ${alertText}`); } console.log(`โœ… Contact Form submitted successfully! (Success state verified)`); } catch (err: any) { console.error(`โŒ Contact Form Test Failed: ${err.message}`); hasErrors = true; } // 4. Test Product Quote Form try { await navigateWithRetry(productUrl, 'Product Quote Form'); // Ensure React has hydrated completely console.log(` โณ Waiting for hydration (5s)...`); await delay(5000); // The product form uses dynamic IDs, so we select by input type in the specific form context try { await page.waitForSelector('form input[type="email"]', { visible: true, timeout: 15000 }); } catch (e) { console.error('Failed to find Product Quote Form input. Page Title:', await page.title()); throw e; } // Wait specifically for hydration logic to initialize the onSubmit handler await page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 2000))); // In RequestQuoteForm, the email input is type="email" and message is a textarea. await page.type('form input[type="email"]', 'testing@mintel.me'); await page.type( 'form textarea', 'Automated request for product quote via E2E testing framework.', ); // Give state a moment to settle await page.evaluate(() => new Promise((resolve) => setTimeout(resolve, 500))); console.log(` โณ Waiting for alert selector (30s)...`); try { // Submit and wait for success state await Promise.all([ page.waitForSelector('[role="alert"]', { timeout: 30000 }), page.click('form button[type="submit"]'), ]); } catch (e) { console.error(`โŒ Timeout waiting for [role="alert"] on Product page. Dumping content:`); const bodyHTML = await page.evaluate(() => document.body.innerHTML.slice(0, 1000)); console.log(`๐Ÿ’ป PAGE BODY (partial): ${bodyHTML}`); throw e; } const alertText = await page.$eval('[role="alert"]', (el) => el.textContent); console.log(` ๐Ÿ”” Alert text: ${alertText}`); const isError = alertText?.toLowerCase().includes('failed') || alertText?.toLowerCase().includes('wrong') || alertText?.toLowerCase().includes('fehlgeschlagen') || alertText?.toLowerCase().includes('schief gelaufen') || alertText?.toLowerCase().includes('fehler'); if (isError) { throw new Error(`Form submitted but showed error: ${alertText}`); } console.log(`โœ… Product Quote Form submitted successfully! (Success state verified)`); } catch (err: any) { console.error(`โŒ Product Quote Form Test Failed: ${err.message}`); hasErrors = true; } console.log(`\n๐Ÿงน Skipping cleanup (No CMS/DB active in this infrastructure).`); await browser.close(); // 6. Evaluation if (hasErrors) { console.error(`\n๐Ÿšจ IMPORTANT: Form E2E checks failed. The CI build is failing.`); process.exit(1); } else { console.log(`\n๐ŸŽ‰ SUCCESS: All form submissions arrived and handled correctly!`); process.exit(0); } } main();