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
282 lines
11 KiB
TypeScript
282 lines
11 KiB
TypeScript
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();
|