From 2ab5a8a41fa7ed030756b343409c1e2a8034b114 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 2 Mar 2026 22:15:49 +0100 Subject: [PATCH] test(e2e): implement full sitemap testing logic as per klz-2026 standards --- apps/web/scripts/check-forms.ts | 182 +++++++++++++++++++++++++++----- 1 file changed, 154 insertions(+), 28 deletions(-) diff --git a/apps/web/scripts/check-forms.ts b/apps/web/scripts/check-forms.ts index d007d8b..1fb227e 100644 --- a/apps/web/scripts/check-forms.ts +++ b/apps/web/scripts/check-forms.ts @@ -3,10 +3,44 @@ import puppeteer from "puppeteer"; const targetUrl = process.env.TEST_URL || "http://localhost:3000"; const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "secret"; +async function fetchSitemapUrls(baseUrl: string): Promise { + const sitemapUrl = `${baseUrl.replace(/\/$/, "")}/sitemap.xml`; + console.log(`๐Ÿ“ฅ Fetching sitemap from ${sitemapUrl}...`); + try { + const response = await fetch(sitemapUrl); + const text = await response.text(); + + // Simple regex to extract loc tags + const matches = text.matchAll(/(.*?)<\/loc>/g); + let urls = Array.from(matches, (m) => m[1]); + + // Normalize to target URL instance + const urlPattern = /https?:\/\/[^\/]+/; + urls = [...new Set(urls)] + .filter((u) => u.startsWith("http")) + .map((u) => u.replace(urlPattern, baseUrl.replace(/\/$/, ""))) + .sort(); + + console.log(`โœ… Found ${urls.length} target URLs.`); + return urls; + } catch (err: any) { + console.error(`โŒ Failed to fetch sitemap: ${err.message}`); + return []; + } +} + async function main() { - console.log(`\n๐Ÿš€ Starting E2E Form Submission Check for: ${targetUrl}`); + console.log(`\n๐Ÿš€ Starting Strict Asset Integrity Check for: ${targetUrl}`); + + let urls = await fetchSitemapUrls(targetUrl); + + if (urls.length === 0) { + console.warn(`โš ๏ธ Falling back to just the homepage.`); + urls = [targetUrl]; + } // Launch browser with KLZ pattern: use system chromium via env + console.log(`\n๐Ÿ•ท๏ธ Launching Puppeteer Headless Engine...`); const browser = await puppeteer.launch({ headless: true, executablePath: @@ -26,23 +60,74 @@ async function main() { const page = await browser.newPage(); - // Enable console logging from the page for debugging - page.on("console", (msg) => console.log(` [PAGE] ${msg.text()}`)); - page.on("pageerror", (err: Error) => - console.error(` [PAGE ERROR] ${err.message}`), - ); - page.on("requestfailed", (req) => - console.error( - ` [REQUEST FAILED] ${req.url()} - ${req.failure()?.errorText}`, - ), - ); + let hasBrokenAssets = false; + let currentScannedUrl = urls[0] || ""; + + // Listen for console logging from the page for debugging + page.on("console", (msg) => { + const type = msg.type(); + // Only capture errors and warnings, not info/logs + if (type === "error" || type === "warn") { + const text = msg.text(); + // Exclude common noise + if ( + text.includes("google-analytics") || + text.includes("googletagmanager") || + text.includes("Fast Refresh") + ) + return; + + console.log(` [PAGE ${type.toUpperCase()}] ${text}`); + } + }); + + page.on("pageerror", (err: Error) => { + if (currentScannedUrl.includes("showcase")) return; + console.error(` [PAGE EXCEPTION] ${err.message}`); + }); + + // Listen to ALL network responses to catch broken assets (404/500) + page.on("response", (response) => { + const status = response.status(); + // Catch classic 404s and 500s on ANY fetch/image/script + if ( + status >= 400 && + status !== 429 && + status !== 999 && + !response.url().includes("google-analytics") && + !response.url().includes("googletagmanager") + ) { + const type = response.request().resourceType(); + + // We explicitly care about images, scripts, stylesheets, and fetches getting 404/500s. + if ( + ["image", "script", "stylesheet", "fetch", "xhr", "document"].includes( + type, + ) + ) { + // Exclude showcase routes from strict sub-asset checking since they proxy external content + if ( + (currentScannedUrl.includes("showcase") || + response.url().includes("showcase")) && + type !== "document" + ) { + return; + } + + console.error( + ` [REQUEST FAILED] ${response.url()} - Status: ${status} (${type})`, + ); + hasBrokenAssets = true; + } + } + }); try { // Authenticate through Gatekeeper console.log(`\n๐Ÿ›ก๏ธ Authenticating through Gatekeeper...`); - console.log(` Navigating to: ${targetUrl}`); + console.log(` Navigating to: ${urls[0]}`); - const response = await page.goto(targetUrl, { + const response = await page.goto(urls[0], { waitUntil: "domcontentloaded", timeout: 60000, }); @@ -69,33 +154,74 @@ async function main() { console.log(`โœ… Already authenticated (no Gatekeeper gate detected).`); } - // Basic smoke test - console.log(`\n๐Ÿงช Testing page load...`); - const title = await page.title(); - console.log(`โœ… Page Title: ${title}`); + // Scan each page + console.log(`\n๐Ÿงช Testing all ${urls.length} pages...`); + for (let i = 0; i < urls.length; i++) { + const u = urls[i]; + currentScannedUrl = u; + console.log(`\n[${i + 1}/${urls.length}] Scanning: ${u}`); + try { + await page.goto(u, { waitUntil: "domcontentloaded", timeout: 60000 }); - if (title.toLowerCase().includes("mintel")) { - console.log(`โœ… Basic smoke test passed!`); - } else { - throw new Error(`Page title mismatch: "${title}"`); + // Simulate a scroll to bottom to trigger lazy-loads if necessary + await page.evaluate(async () => { + await new Promise((resolve) => { + let totalHeight = 0; + const distance = 500; + const timer = setInterval(() => { + const scrollHeight = document.body.scrollHeight; + window.scrollBy(0, distance); + totalHeight += distance; + // Stop scrolling if we reached the bottom or scrolled for more than 5 seconds + if (totalHeight >= scrollHeight || totalHeight > 10000) { + clearInterval(timer); + resolve(); + } + }, 100); + }); + }); + + // Small delay for final hydration and asynchronous asset loading + await new Promise((resolve) => setTimeout(resolve, 1500)); + + const title = await page.title(); + console.log(` โœ… Page Title: ${title}`); + + if (!title) { + throw new Error(`Page title is missing.`); + } + } catch (err: any) { + console.error( + ` โŒ Timeout or navigation error on ${u}: ${err.message}`, + ); + hasBrokenAssets = true; + } } } catch (err: any) { - console.error(`โŒ Test Failed: ${err.message}`); - // Take a screenshot for debugging + console.error(`\nโŒ Fatal Test Error: ${err.message}`); + // Take a screenshot for debugging on crash try { const screenshotPath = "/tmp/e2e-failure.png"; await page.screenshot({ path: screenshotPath, fullPage: true }); console.log(`๐Ÿ“ธ Screenshot saved to ${screenshotPath}`); } catch { - /* ignore screenshot errors */ + /* ignore */ } - console.log(` Current URL: ${page.url()}`); - await browser.close(); - process.exit(1); + hasBrokenAssets = true; } await browser.close(); - console.log(`\n๐ŸŽ‰ SUCCESS: E2E smoke test passed!`); + + if (hasBrokenAssets) { + console.error( + `\n๐Ÿšจ The CI build will now fail to prevent bad code from reaching production.`, + ); + process.exit(1); + } + + console.log( + `\n๐ŸŽ‰ SUCCESS: All ${urls.length} pages rendered perfectly with 0 broken assets!`, + ); process.exit(0); }