Files
klz-cables.com/scripts/check-forms.ts
Marc Mintel 03d10f9a83
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Successful in 2m23s
Build & Deploy / 🏗️ Build (push) Successful in 3m49s
Build & Deploy / 🚀 Deploy (push) Successful in 23s
Build & Deploy / 🧪 Post-Deploy Verification (push) Failing after 5m17s
Build & Deploy / 🔔 Notify (push) Successful in 2s
test: refine E2E form verification with German error keyword support
2026-03-11 09:45:10 +01:00

264 lines
9.8 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';
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/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,
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();
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) => {
console.error('💻 BROWSER REQUEST FAILED:', request.url(), request.failure()?.errorText);
});
// 3. Authenticate through Gatekeeper login form
console.log(`\n🛡 Authenticating through Gatekeeper...`);
try {
// Navigate to a protected page so Gatekeeper redirects us to the login screen
await page.goto(contactUrl, { waitUntil: 'networkidle0', timeout: 30000 });
// Check if we landed on the Gatekeeper login page
const isGatekeeperPage = await page.$('input[name="password"]');
if (isGatekeeperPage) {
await page.type('input[name="password"]', gatekeeperPassword);
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle0', timeout: 30000 }),
page.click('button[type="submit"]'),
]);
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 {
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. 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)));
// 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. 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#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();