The browser harness lived in /tmp/opencode and an overnight reboot wiped it. It now lives in tests/ and survives reboots, is versioned and runs with one command. - tests/lib.mjs: shared helpers (Chromium auto-detect, seeding, IndexedDB fixtures, stub/extra instance spawn with process-group cleanup, check/finish). - tests/run-all.mjs + npm run test:e2e [filter]: runs the ten suites in order with a summary; suites that need a stub Ollama or VAPID keys start their own next start instance on 3208/3209, verify the port is free and kill the group on exit. Test scratch in tests/.tmp/ (gitignored). - Suites rebuilt and green against the current build: regression 38, mobile 94 (tab matrix, ... sheet, settings sheet, axe), wizard 83, native 31 (PWA/offline/share/back/undo/haptics/draft), i18n 13, freeze-loading 6, review 15, phase1 36 (backup/sync/usage limits), phase2 42 (learning engine/reminders), phase3 21 (homework check). 379 checks, 10/10 suites in one full run. - playwright-core + axe-core added as devDependencies; CHROME env or the newest ~/.cache/ms-playwright chromium is used. - README: new decision 45 documenting the harness; decision 44's verification note updated. The older suites' exact sources were lost with the wipe, so the rebuilt versions reconstruct the same feature areas rather than being byte-identical.
71 lines
3.6 KiB
JavaScript
71 lines
3.6 KiB
JavaScript
/**
|
|
* i18n suite: EN/CS dictionary parity, the locale switch, Czech copy on the
|
|
* desktop shell and the phone tab bar, and Czech plurals.
|
|
*/
|
|
import { readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
|
|
import { BASE, DECK, REPO, launchBrowser, makeChecker, makePage, seed } from "./lib.mjs";
|
|
|
|
const { check, finish } = makeChecker();
|
|
const browser = await launchBrowser();
|
|
|
|
/* 1. dictionary parity ----------------------------------------------- */
|
|
{
|
|
const source = readFileSync(path.join(REPO, "lib/i18n-dict.ts"), "utf8");
|
|
const split = source.indexOf("const cs");
|
|
const pattern = /^\s{2}"([^"]+)":/gm;
|
|
const en = [...source.slice(0, split).matchAll(pattern)].map((match) => match[1]);
|
|
const cs = [...source.slice(split).matchAll(pattern)].map((match) => match[1]);
|
|
const missingInCs = en.filter((key) => !cs.includes(key));
|
|
const missingInEn = cs.filter((key) => !en.includes(key));
|
|
check("key parity: same keys in both dictionaries", missingInCs.length === 0 && missingInEn.length === 0, JSON.stringify({ missingInCs, missingInEn }).slice(0, 200));
|
|
check("dictionary is substantial", en.length > 700, String(en.length));
|
|
}
|
|
|
|
/* 2. Czech desktop shell --------------------------------------------- */
|
|
{
|
|
const { context, page } = await makePage(browser, { width: 1280, height: 900, deviceScaleFactor: 1 });
|
|
await seed(page, { locale: "cs", decks: [DECK] });
|
|
await page.goto(BASE, { waitUntil: "networkidle" });
|
|
await page.waitForTimeout(900);
|
|
check("library title is Czech", await page.getByRole("heading", { name: "Tvoje mraznička" }).isVisible());
|
|
check("sidebar says Všechny sady", await page.getByRole("button", { name: /Všechny sady/ }).first().isVisible());
|
|
check("sidebar says Nezařazené", await page.getByRole("button", { name: /Nezařazené/ }).first().isVisible());
|
|
check("deck card CTA is Studovat", (await page.getByRole("link", { name: "Studovat" }).count()) === 1);
|
|
check("Czech plural on the card (2 aktivity)", (await page.getByText("2 aktivity").filter({ visible: true }).count()) >= 1);
|
|
check("review card is Czech", await page.getByText("Chytré opakování").isVisible());
|
|
check("desktop has no tab bar visible", !(await page.locator("nav.tabbar").isVisible()));
|
|
|
|
// Locale switch from the settings dialog (desktop footer).
|
|
await page.locator("aside").getByRole("button", { name: "Nastavení" }).click();
|
|
const dialog = page.getByRole("dialog");
|
|
await dialog.waitFor({ timeout: 6000 });
|
|
await page.waitForTimeout(600);
|
|
await dialog.locator(".segmented").first().getByRole("button", { name: /English/ }).click();
|
|
await page.waitForTimeout(500);
|
|
await page.keyboard.press("Escape");
|
|
await page.waitForTimeout(600);
|
|
check("locale switch to English updates the UI", await page.getByRole("heading", { name: "Your icebox" }).isVisible());
|
|
await context.close();
|
|
}
|
|
|
|
/* 3. Czech phone tab bar + review card ------------------------------- */
|
|
{
|
|
const { context, page } = await makePage(browser);
|
|
await seed(page, { locale: "cs", decks: [DECK] });
|
|
await page.goto(BASE, { waitUntil: "networkidle" });
|
|
await page.waitForTimeout(900);
|
|
const tabTexts = (await page.locator("nav.tabbar").locator("a, button").allInnerTexts()).map((text) => text.trim());
|
|
check(
|
|
"cs tab labels are the short ones",
|
|
JSON.stringify(tabTexts) === JSON.stringify(["Knihovna", "Pokrok", "Nová sada", "Zkouška", "Nastavení"]),
|
|
JSON.stringify(tabTexts),
|
|
);
|
|
check("cs review card present", await page.getByText("Chytré opakování").isVisible());
|
|
check("cs exam tab label is not the long one", !tabTexts.includes("Zkouškový režim"));
|
|
await context.close();
|
|
}
|
|
|
|
await browser.close();
|
|
finish();
|