85 lines
4.8 KiB
TypeScript
85 lines
4.8 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { createPrivateKey, sign } from "node:crypto";
|
|
import { mkdir } from "node:fs/promises";
|
|
import { expect, test } from "@playwright/test";
|
|
|
|
// Language switching in a real browser, Ory-free (the visual stack). Proves the whole path a
|
|
// visitor takes: pick a language, read the page in it, and stay in it while clicking around —
|
|
// including into a plugin, whose words come from its own catalog (plugins/scheduling/i18n/).
|
|
|
|
const BASE_URL = process.env.BASE_URL ?? "http://localhost:3000";
|
|
const SESSION_COOKIE = "plainpages_jwt";
|
|
const SHOTS = "artifacts/screenshots";
|
|
|
|
// Same trick as visual.spec.ts: sign a session JWT with the committed dev tokenizer key so the
|
|
// gated pages render without standing up Ory.
|
|
function devSession(permissions: string[] = []): string {
|
|
const jwk = JSON.parse(readFileSync("/repo/jwks.json", "utf8")).keys[0];
|
|
const key = createPrivateKey({ format: "jwk", key: jwk });
|
|
const b64 = (o: unknown): string => Buffer.from(JSON.stringify(o)).toString("base64url");
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const input = `${b64({ alg: "ES256", kid: jwk.kid, typ: "JWT" })}.${b64({ email: "demo@plainpages.local", exp: now + 3600, iat: now, permissions, sub: "lang-demo" })}`;
|
|
return `${input}.${sign("SHA256", Buffer.from(input), { dsaEncoding: "ieee-p1363", key }).toString("base64url")}`;
|
|
}
|
|
|
|
test("the switcher changes language, and the choice survives clicking through the app", async ({ page, context }) => {
|
|
await context.addCookies([{ name: SESSION_COOKIE, url: BASE_URL, value: devSession(["scheduling:read"]) }]);
|
|
|
|
await page.goto("/dashboard");
|
|
await expect(page.locator("html")).toHaveAttribute("lang", "en-US");
|
|
await expect(page.getByRole("link", { name: "Dashboard" })).toBeVisible();
|
|
|
|
// The picker sits in the sidebar footer beside the theme switch; each entry is a plain link to
|
|
// this same page in that language (zero-JS).
|
|
await page.locator('button[aria-label="Language"]').click();
|
|
await page.getByRole("link", { name: /svenska/i }).click();
|
|
|
|
await expect(page.locator("html")).toHaveAttribute("lang", "sv-SE");
|
|
await expect(page).toHaveURL(/locale=sv-SE/);
|
|
await expect(page.getByRole("heading", { name: "Startpanel" })).toBeVisible(); // the starter dashboard, in Swedish
|
|
await expect(page.getByRole("link", { name: "Översikt", exact: true })).toBeVisible(); // the menu too
|
|
await mkdir(SHOTS, { recursive: true });
|
|
await page.screenshot({ fullPage: true, path: `${SHOTS}/live-05-swedish.png` });
|
|
|
|
// Clicking a menu item keeps Swedish — the host carries the choice onto the links it renders,
|
|
// and the plugin's own page is translated from its own catalog. The section's own label comes
|
|
// from the plugin's catalog too, so opening it proves the nav fragment was translated.
|
|
await page.locator('summary[aria-label="Visa eller dölj Schemaläggning"]').click();
|
|
await page.getByRole("link", { name: "Pass", exact: true }).click();
|
|
await expect(page).toHaveURL(/\/scheduling\/shifts\?locale=sv-SE/);
|
|
await expect(page.locator("html")).toHaveAttribute("lang", "sv-SE");
|
|
await expect(page.getByRole("heading", { name: "Pass" })).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "Sök" })).toBeVisible(); // the core filter bar, in Swedish
|
|
|
|
// The filter bar is a GET form: submitting it replaces the whole query string, so the choice
|
|
// survives only because the form carries it as a hidden field.
|
|
await page.getByRole("button", { name: "Sök" }).click();
|
|
await expect(page).toHaveURL(/locale=sv-SE/);
|
|
await expect(page.locator("html")).toHaveAttribute("lang", "sv-SE");
|
|
|
|
// …and back to English the same way.
|
|
await page.locator('button[aria-label="Språk"]').click();
|
|
await page.getByRole("link", { name: /English/i }).click();
|
|
await expect(page.locator("html")).toHaveAttribute("lang", "en-US");
|
|
await expect(page.getByRole("heading", { name: "Shifts" })).toBeVisible();
|
|
});
|
|
|
|
test("a browser that asks for Swedish gets it without touching the URL", async ({ browser }) => {
|
|
const context = await browser.newContext({ locale: "sv" }); // a browser set to Swedish, no region
|
|
const page = await context.newPage();
|
|
await page.goto(`${BASE_URL}/`);
|
|
|
|
await expect(page.locator("html")).toHaveAttribute("lang", "sv-SE");
|
|
const signIn = page.locator("#main-content").getByRole("link", { name: "Logga in" });
|
|
await expect(signIn).toBeVisible();
|
|
// Nothing was chosen in the URL, so the links stay plain — the browser asks again on the next hit.
|
|
await expect(signIn).toHaveAttribute("href", "/login");
|
|
await context.close();
|
|
});
|
|
|
|
test("an uninstalled language falls back to English rather than failing", async ({ page }) => {
|
|
const response = await page.goto("/?locale=sv-FI"); // sv-SE is installed; sv-FI is not
|
|
expect(response?.status()).toBe(200);
|
|
await expect(page.locator("html")).toHaveAttribute("lang", "en-US");
|
|
});
|