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'; async function main() { console.log(`\n๐Ÿš€ Starting E2E Form Submission Check for: ${targetUrl}`); // 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/contact') || u.includes('/de/kontakt')); // Ensure we select an actual product page (depth >= 4 segments: /de/produkte/category/product) const productUrl = urls.find( (u) => (u.includes('/de/produkte/') || u.includes('/de/products/')) && new URL(u).pathname.split('/').filter(Boolean).length >= 4, ); if (!contactUrl) { console.error( `โŒ Could not find contact page in sitemap. Checked patterns: /de/contact, /de/kontakt`, ); console.log('Available URLs (first 20):', urls.slice(0, 20)); process.exit(1); } if (!productUrl) { console.error( `โŒ Could not find a product page in sitemap. Checked patterns: /de/produkte/, /de/products/`, ); console.log('Available URLs (first 20):', urls.slice(0, 20)); 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, executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROME_PATH || undefined, args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'], }); const page = await browser.newPage(); // Set viewport for consistent layout await page.setViewport({ width: 1280, height: 800 }); page.on('console', (msg) => console.log('๐Ÿ’ป BROWSER CONSOLE:', msg.text())); page.on('pageerror', (error: any) => console.error('๐Ÿ’ป BROWSER ERROR:', error.message)); page.on('requestfailed', (request) => { // Only log failures for main document and API calls to reduce noise const resourceType = request.resourceType(); if (resourceType === 'document' || resourceType === 'fetch' || resourceType === 'xhr') { console.error('๐Ÿ’ป BROWSER REQUEST FAILED:', request.url(), request.failure()?.errorText); } }); // 3. Authenticate through Gatekeeper via Direct Cookie Insertion console.log(`\n๐Ÿ›ก๏ธ Authenticating through Gatekeeper via Cookie Injection...`); try { const domain = new URL(targetUrl).hostname; await page.setCookie({ name: 'klz_gatekeeper_session', value: gatekeeperPassword, domain: domain, path: '/', secure: true, httpOnly: true, }); console.log(`โœ… Gatekeeper cookie injected for domain: ${domain}`); } catch (err: any) { console.error(`โŒ Gatekeeper cookie injection failed: ${err.message}`); await browser.close(); process.exit(1); } let hasErrors = false; // 4. Test Contact Form try { console.log(`\n๐Ÿงช Testing Contact Form on: ${contactUrl}`); await page.goto(contactUrl, { waitUntil: 'networkidle0', timeout: 30000 }); // Ensure React has hydrated completely await page.waitForNetworkIdle({ idleTime: 1000, timeout: 15000 }).catch(() => {}); // Ensure form is visible and interactive try { // Find the form input by name await page.waitForSelector('form#contact-form input[name="name"]', { visible: true, timeout: 15000, }); } catch (e) { console.error('โŒ Failed to find Contact Form input.'); console.log('Page Title:', await page.title()); const bodySnippet = await page.evaluate(() => document.body.innerText.slice(0, 500)); console.log('Page Content Snippet:', bodySnippet); 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('form#contact-form input[name="name"]', 'Automated E2E Test'); await page.type('form#contact-form input[name="email"]', 'testing@mintel.me'); await page.type( 'form#contact-form 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(` Submitting Contact Form...`); // Explicitly click submit and wait for success state (using the success Card role="alert") await Promise.all([ page.waitForSelector('[role="alert"]', { timeout: 15000 }), page.click('form#contact-form button[type="submit"]'), ]); const alertText = await page.$eval('[role="alert"]', (el) => el.textContent); console.log(` Alert text: ${alertText}`); const errorKeywords = ['Failed', 'went wrong', 'fehlgeschlagen', 'schief gelaufen']; if (errorKeywords.some((kw) => alertText?.toLowerCase().includes(kw.toLowerCase()))) { throw new Error(`Form submitted but showed error state: ${alertText}`); } console.log(`โœ… Contact Form submitted successfully! (Success state verified via alert text)`); } catch (err: any) { console.error(`โŒ Contact Form Test Failed: ${err.message}`); hasErrors = true; } // 4. Test Product Quote Form try { console.log(`\n๐Ÿงช Testing Product Quote Form on: ${productUrl}`); await page.goto(productUrl, { waitUntil: 'networkidle0', timeout: 30000 }); // Ensure React has hydrated completely await page.waitForNetworkIdle({ idleTime: 1000, timeout: 15000 }).catch(() => {}); // The product form uses dynamic IDs, so we select by input type in the specific form context try { await page.waitForSelector('form#quote-request-form input[type="email"]', { visible: true, timeout: 15000, }); } catch (e) { console.error('โŒ Failed to find Product Quote Form input.'); console.log('Page Title:', await page.title()); const bodySnippet = await page.evaluate(() => document.body.innerText.slice(0, 500)); console.log('Page Content Snippet:', bodySnippet); 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#quote-request-form input[type="email"]', 'testing@mintel.me'); await page.type( 'form#quote-request-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(` Submitting Product Quote Form...`); // Submit and wait for success state await Promise.all([ page.waitForSelector('[role="alert"]', { timeout: 15000 }), page.click('form#quote-request-form button[type="submit"]'), ]); const alertText = await page.$eval('[role="alert"]', (el) => el.textContent); console.log(` Alert text: ${alertText}`); const errorKeywords = ['Failed', 'went wrong', 'fehlgeschlagen', 'schief gelaufen']; if (errorKeywords.some((kw) => alertText?.toLowerCase().includes(kw.toLowerCase()))) { throw new Error(`Product Quote Form submitted but showed error state: ${alertText}`); } console.log( `โœ… Product Quote Form submitted successfully! (Success state verified via alert text)`, ); } catch (err: any) { console.error(`โŒ Product Quote Form Test Failed: ${err.message}`); hasErrors = true; } // 5. Cleanup: Delete test submissions from Payload CMS console.log(`\n๐Ÿงน Starting cleanup of test submissions...`); try { const apiUrl = `${targetUrl.replace(/\/$/, '')}/api/form-submissions`; const searchUrl = `${apiUrl}?where[email][equals]=testing@mintel.me`; // Fetch test submissions const searchResponse = await axios.get(searchUrl, { headers: { Cookie: `klz_gatekeeper_session=${gatekeeperPassword}` }, }); const testSubmissions = searchResponse.data.docs || []; console.log(` Found ${testSubmissions.length} test submissions to clean up.`); for (const doc of testSubmissions) { try { await axios.delete(`${apiUrl}/${doc.id}`, { headers: { Cookie: `klz_gatekeeper_session=${gatekeeperPassword}` }, }); console.log(` โœ… Deleted submission: ${doc.id}`); } catch (delErr: any) { // Log but don't fail, 403s on Directus / Payload APIs for guest Gatekeeper sessions are normal console.warn( ` โš ๏ธ Cleanup attempt on ${doc.id} returned an error, typically due to API Auth separation: ${delErr.message}`, ); } } } catch (err: any) { if (err.response?.status === 403) { console.warn( ` โš ๏ธ Cleanup fetch failed with 403 Forbidden. This is expected if the runner lacks admin API credentials. Test submissions remain in the database.`, ); } else { console.error(` โŒ Cleanup fetch failed: ${err.message}`); } // Don't mark the whole test as failed just because cleanup failed } 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();