Files
e-tib.com/scripts/check-forms.ts
Marc Mintel 26d325df44
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 38s
Build & Deploy / 🧪 QA (push) Successful in 1m22s
Build & Deploy / 🏗️ Build (push) Successful in 4m27s
Build & Deploy / 🚀 Deploy (push) Failing after 39s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
fix(config): ensure analytics URL has protocol to prevent build crash
2026-05-12 13:01:18 +02:00

368 lines
13 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();