fix(config): ensure analytics URL has protocol to prevent build crash
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

This commit is contained in:
2026-05-12 13:01:18 +02:00
parent 5d3be82d8f
commit 26d325df44
32998 changed files with 7245 additions and 3832721 deletions

367
scripts/check-forms.ts Normal file
View File

@@ -0,0 +1,367 @@
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();