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 || 'klz2026'; // Expected slug translations: German key → English value const SLUG_MAP: Record = { produkte: 'products', kontakt: 'contact', niederspannungskabel: 'low-voltage-cables', mittelspannungskabel: 'medium-voltage-cables', hochspannungskabel: 'high-voltage-cables', solarkabel: 'solar-cables', impressum: 'legal-notice', datenschutz: 'privacy-policy', agbs: 'terms', }; // Reverse map: English → German const REVERSE_SLUG_MAP: Record = Object.fromEntries( Object.entries(SLUG_MAP).map(([de, en]) => [en, de]), ); const headers = { Cookie: `klz_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 (err: any) { 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 (err: any) { 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); });