Files
klz-cables.com/scripts/check-forms.ts
Marc Mintel 040809812a
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Failing after 2m24s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 6s
test: Fix form E2E timeouts by using JS native click to bypass overlays
2026-03-11 14:54:21 +01:00

282 lines
11 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/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.$eval('form#contact-form button[type="submit"]', (el) =>
(el as HTMLButtonElement).click(),
),
]);
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}`);
await page.screenshot({ path: 'contact-form-error.png', fullPage: true });
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.$eval('form#quote-request-form button[type="submit"]', (el) =>
(el as HTMLButtonElement).click(),
),
]);
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();