import axios from "axios"; import * as cheerio from "cheerio"; /** * Locale & Language Switcher Smoke Test * * For every URL in the sitemap: * 1. Fetches the page HTML * 2. Extracts tags * 3. Verifies each alternate URL uses correctly translated slugs * 4. Verifies each alternate URL returns HTTP 200 */ const targetUrl = process.argv[2] || process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"; const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "lassmichrein"; // Expected slug translations: German key → English value const SLUG_MAP: Record = { // Add translations if mb-grid translates URLs: e.g. produkte: 'products' }; // Reverse map: English → German const REVERSE_SLUG_MAP: Record = Object.fromEntries( Object.entries(SLUG_MAP).map(([de, en]) => [en, de]), ); const headers = { Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}` }; function getExpectedTranslation( sourcePath: string, sourceLocale: string, targetLocale: string, ): string { const segments = sourcePath.split("/").filter(Boolean); // First segment is locale segments[0] = targetLocale; const map = sourceLocale === "de" ? SLUG_MAP : REVERSE_SLUG_MAP; return ( "/" + segments .map((seg, i) => { if (i === 0) return seg; // locale return map[seg] || seg; // translate or keep (product names like n2x2y stay the same) }) .join("/") ); } async function main() { console.log(`\n🌐 Starting Locale Smoke Test for: ${targetUrl}\n`); // 1. Fetch sitemap const sitemapUrl = `${targetUrl.replace(/\/$/, "")}/sitemap.xml`; console.log(`šŸ“„ Fetching sitemap from ${sitemapUrl}...`); const sitemapRes = await axios.get(sitemapUrl, { headers, validateStatus: (s) => s < 400, }); const $sitemap = cheerio.load(sitemapRes.data, { xmlMode: true }); let urls = $sitemap("url loc") .map((_i, el) => $sitemap(el).text()) .get(); const urlPattern = /https?:\/\/[^/]+/; urls = [...new Set(urls)] .filter((u) => u.startsWith("http")) .map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, ""))) .sort(); console.log(`āœ… Found ${urls.length} URLs in sitemap.\n`); let totalChecked = 0; let totalPassed = 0; let totalFailed = 0; const failures: string[] = []; for (const url of urls) { const path = new URL(url).pathname; const locale = path.split("/")[1]; if (!locale || !["de", "en"].includes(locale)) continue; try { const res = await axios.get(url, { headers, validateStatus: null }); if (res.status >= 400) continue; // Skip pages that are already broken (check-http catches those) const $ = cheerio.load(res.data); // Extract hreflang alternate links const alternates: { hreflang: string; href: string }[] = []; $('link[rel="alternate"][hreflang]').each((_i, el) => { const hreflang = $(el).attr("hreflang") || ""; let href = $(el).attr("href") || ""; if (href && hreflang && hreflang !== "x-default") { href = href.replace(urlPattern, targetUrl.replace(/\/$/, "")); alternates.push({ hreflang, href }); } }); if (alternates.length === 0) { // Some pages may not have alternates, that's OK continue; } totalChecked++; // Validate each alternate let pageOk = true; for (const alt of alternates) { if (alt.hreflang === locale) continue; // Same locale, skip // 1. Check slug translation is correct const expectedPath = getExpectedTranslation(path, locale, alt.hreflang); const actualPath = new URL(alt.href).pathname; if (actualPath !== expectedPath) { console.error( `āŒ SLUG MISMATCH: ${path} → hreflang="${alt.hreflang}" expected ${expectedPath} but got ${actualPath}`, ); failures.push( `Slug mismatch: ${path} → ${alt.hreflang}: expected ${expectedPath}, got ${actualPath}`, ); pageOk = false; continue; } // 2. Check alternate URL returns 200 try { const altRes = await axios.get(alt.href, { headers, validateStatus: null, maxRedirects: 5, }); if (altRes.status >= 400) { console.error( `āŒ BROKEN ALTERNATE: ${path} → ${alt.href} returned ${altRes.status}`, ); failures.push( `Broken alternate: ${path} → ${alt.href} (${altRes.status})`, ); pageOk = false; } } catch (error) { const err = error as Error; console.error( `āŒ NETWORK ERROR: ${path} → ${alt.href}: ${err.message}`, ); failures.push(`Network error: ${path} → ${alt.href}: ${err.message}`); pageOk = false; } } if (pageOk) { console.log( `āœ… ${path} — alternates OK (${alternates .map((a) => a.hreflang) .filter((h) => h !== locale) .join(", ")})`, ); totalPassed++; } else { totalFailed++; } } catch (error) { const err = error as Error; console.error(`āŒ NETWORK ERROR fetching ${url}: ${err.message}`); totalFailed++; } } console.log(`\n${"─".repeat(60)}`); console.log(`šŸ“Š Locale Smoke Test Results:`); console.log(` Pages checked: ${totalChecked}`); console.log(` Passed: ${totalPassed}`); console.log(` Failed: ${totalFailed}`); if (failures.length > 0) { console.log(`\nāŒ Failures:`); failures.forEach((f) => console.log(` • ${f}`)); console.log(`\nāŒ Locale Smoke Test FAILED.`); process.exit(1); } else { console.log( `\n✨ All locale alternates are correctly translated and reachable!`, ); process.exit(0); } } main().catch((err) => { console.error(`\nāŒ Critical error:`, err.message); process.exit(1); });