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);
});