Files
SnapOtter/tests/e2e-landing/i18n-seo-rtl.spec.ts
T
SnapOtterandGitHub 67f54347b2 fix(landing): keep mixed-case locale casing in emitted URLs (#562)
Astro's getRelativeLocaleUrl lowercases the locale segment by default, so landing links and hreflang for zh-CN, zh-TW, and pt-BR were emitted lowercase and 404 on case-sensitive Cloudflare Pages. Pin the casing at the localizeHref chokepoint with normalizeLocale: false, add an e2e hreflang casing guard, and add a deploy-time check that blocks the build if any lowercased locale path leaks into the output.

Closes #554
2026-07-18 10:47:53 +08:00

77 lines
3.4 KiB
TypeScript

// tests/e2e-landing/i18n-seo-rtl.spec.ts
import { expect, test } from "@playwright/test";
test.describe("landing hreflang, RTL, and banner", () => {
test("emits reciprocal hreflang alternates including x-default", async ({ page }) => {
await page.goto("/faq");
const en = await page.locator('link[rel="alternate"][hreflang="en"]').getAttribute("href");
const de = await page.locator('link[rel="alternate"][hreflang="de"]').getAttribute("href");
const xd = await page
.locator('link[rel="alternate"][hreflang="x-default"]')
.getAttribute("href");
// Astro's `format: "directory"` emits trailing-slash URLs, so the alternates
// carry a trailing slash. What matters is reciprocity and that x-default == en.
expect(en).toBe("https://snapotter.com/faq/");
expect(de).toBe("https://snapotter.com/de/faq/");
expect(xd).toBe("https://snapotter.com/faq/");
expect(xd).toBe(en);
// Reciprocity: the German page points back to the same set.
await page.goto("/de/faq");
const enFromDe = await page
.locator('link[rel="alternate"][hreflang="en"]')
.getAttribute("href");
expect(enFromDe).toBe("https://snapotter.com/faq/");
});
// Regression guard for #554. Astro's getRelativeLocaleUrl lowercases the locale
// segment by default (normalizeTheLocale: "zh-CN" -> "zh-cn"), but the built
// directories and the sitemap keep canonical mixed case. The lowercased links
// resolve on case-insensitive local hosting but 404 on Cloudflare Pages. This
// reads the emitted href string, so it catches the mismatch regardless of the
// local filesystem's case sensitivity (which is why the specs above missed it).
test("mixed-case locales keep their exact casing in hreflang links", async ({ page }) => {
await page.goto("/faq");
for (const code of ["zh-CN", "zh-TW", "pt-BR"]) {
const href = await page
.locator(`link[rel="alternate"][hreflang="${code}"]`)
.getAttribute("href");
expect(href, `hreflang ${code} must preserve casing`).toBe(
`https://snapotter.com/${code}/faq/`,
);
// The lowercased path (the bug) must not appear.
expect(href).not.toContain(code.toLowerCase());
}
});
test("Arabic renders dir=rtl on <html>", async ({ page }) => {
await page.goto("/ar/faq");
await expect(page.locator("html")).toHaveAttribute("dir", "rtl");
await expect(page.locator("html")).toHaveAttribute("lang", "ar");
});
test("English page sets og:locale to en_US", async ({ page }) => {
await page.goto("/faq");
await expect(page.locator('meta[property="og:locale"]')).toHaveAttribute("content", "en");
});
test("machine-translation banner appears on de and dismisses persistently", async ({ page }) => {
await page.goto("/de/faq");
const banner = page.locator("[data-mt-banner]");
await expect(banner).toBeVisible();
await banner.locator("[data-mt-dismiss]").click();
await expect(banner).toBeHidden();
const stored = await page.evaluate(() => localStorage.getItem("snapotter:mt-banner"));
expect(stored).toBe("dismissed");
// Reload: banner stays hidden.
await page.reload();
await expect(page.locator("[data-mt-banner]")).toBeHidden();
});
test("English page shows no machine-translation banner", async ({ page }) => {
await page.goto("/faq");
await expect(page.locator("[data-mt-banner]")).toHaveCount(0);
});
});