Files
rand-tools/tests/browser/app.spec.ts
T
2026-09-01 08:27:00 +02:00

164 lines
6.2 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
const ORIGIN = "http://127.0.0.1:4173";
async function localOnly(page: Page) {
const external: string[] = [];
await page.route("**/*", async (route) => {
const url = new URL(route.request().url());
if (url.origin !== ORIGIN) {
external.push(url.href);
await route.abort();
} else await route.continue();
});
return external;
}
test("runs from a nested path without external requests", async ({ page }) => {
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(error.message));
page.on("console", (message) => {
if (message.type() === "error") errors.push(message.text());
});
const external = await localOnly(page);
await page.goto("/deep/nested/rand/");
await expect(
page.getByRole("heading", { name: "Random Tools" }),
).toBeVisible();
expect(external).toEqual([]);
expect(errors).toEqual([]);
});
test("repeats deterministic output without external requests", async ({
page,
}) => {
const external = await localOnly(page);
await page.goto("/deep/nested/rand/");
await page.getByLabel("Random source").selectOption("deterministic");
await page
.getByRole("textbox", { name: "Seed", exact: true })
.fill("browser-recipe");
await page.getByLabel("Count").fill("5");
await page.getByRole("button", { name: "Generate numbers" }).click();
const result = page.locator(".result > pre");
const first = await result.textContent();
expect(first?.trim().split("\n")).toHaveLength(5);
await page.getByRole("button", { name: "Generate numbers" }).click();
await expect(result).toHaveText(first ?? "");
await expect(
page.getByText(/deterministic-non-cryptographic/u),
).toBeAttached();
expect(external).toEqual([]);
});
test("runs the local draw catalogue with recipe metadata", async ({ page }) => {
const external = await localOnly(page);
await page.goto("/deep/nested/rand/");
await page.getByLabel("Random source").selectOption("deterministic");
await page
.getByRole("textbox", { name: "Seed", exact: true })
.fill("draw-catalogue");
await page.getByRole("tab", { name: "Draws" }).click();
await page.getByLabel("Result count").fill("7");
await page.getByRole("button", { name: "Generate draw" }).click();
await expect(page.locator(".result > pre")).toContainText(/Heads|Tails/u);
await page.getByLabel("Draw type").selectOption("cards");
await page.getByLabel("Cards to deal").fill("5");
await page.getByRole("button", { name: "Generate draw" }).click();
await expect(page.locator(".result > pre")).toContainText(/[♠♥♦♣]/u);
await page.getByLabel("Draw type").selectOption("sequence");
await page.getByLabel("Sequence minimum").fill("5");
await page.getByLabel("Sequence maximum (inclusive)").fill("8");
await page.getByRole("button", { name: "Generate draw" }).click();
const sequence = (await page.locator(".result > pre").textContent())
?.trim()
.split("\n")
.map(Number)
.sort((a, b) => a - b);
expect(sequence).toEqual([5, 6, 7, 8]);
await page.getByLabel("Draw type").selectOption("dates");
await page.getByLabel("Result count").fill("3");
await page.getByLabel("Start date (inclusive)").fill("2026-01-01");
await page.getByLabel("End date (inclusive)").fill("2026-01-31");
await page.getByLabel("Do not repeat dates").check();
await page.getByRole("button", { name: "Generate draw" }).click();
await expect(page.locator(".result > pre")).toContainText(/2026-01-/u);
await page.getByLabel("Draw type").selectOption("decimals");
await page.getByLabel("Result count").fill("2");
await page.getByLabel("Decimal places").fill("4");
await page.getByRole("button", { name: "Generate draw" }).click();
await expect(page.locator(".result > pre")).toContainText(/0\.\d{4}/u);
await page.getByLabel("Draw type").selectOption("coordinates");
await page.getByLabel("Result count").fill("2");
await page.getByLabel("Coordinate decimal places").fill("4");
await page.getByRole("button", { name: "Generate draw" }).click();
await expect(page.locator(".result > pre")).toContainText(
/-?\d+\.\d{4}, -?\d+\.\d{4}/u,
);
await expect(
page.getByText(/mathematical samples on a spherical model/u),
).toBeVisible();
expect(external).toEqual([]);
});
test("identifies normalized custom passphrase inputs without announcing output", async ({
page,
}) => {
const external = await localOnly(page);
await page.goto("/deep/nested/rand/");
await page.getByLabel("Random source").selectOption("deterministic");
await page
.getByRole("textbox", { name: "Seed", exact: true })
.fill("word-list-identity");
await page.getByRole("tab", { name: "Passphrases" }).click();
await page
.getByLabel(/Optional custom word list/u)
.fill(" alpha \n\nbeta\n gamma ");
await page.getByRole("button", { name: "Generate passphrase" }).click();
const result = page.locator(".result");
await expect(result.getByRole("status")).toHaveText(
/Result ready: Passphrase/u,
);
await expect(result).not.toHaveAttribute("aria-live");
await result.getByText("Reproduction metadata").click();
await expect(result.locator("details pre")).toContainText(
/"normalizedCount": 3/u,
);
await expect(result.locator("details pre")).toContainText(
/"sha256": "[0-9a-f]{64}"/u,
);
await expect(result.locator("details pre")).toContainText(
/"customInputRequiredForReproduction": true/u,
);
expect(external).toEqual([]);
});
test("serves the release identity and hardened headers", async ({
request,
}) => {
const index = await request.get("/deep/nested/rand/");
expect(index.ok()).toBe(true);
expect(index.headers()["content-security-policy"]).toContain(
"default-src 'self'",
);
expect(index.headers()["content-security-policy"]).toContain(
"connect-src 'self'",
);
expect(index.headers()["content-security-policy"]).not.toMatch(
/connect-src[^;]*https?:/u,
);
expect(await index.text()).not.toMatch(/\b(?:src|href)=["']\//u);
const manifest = await request.get("/deep/nested/rand/toolbox-app.json");
await expect(manifest.json()).resolves.toMatchObject({
id: "de.add-ideas.rand-tools",
version: "0.1.1",
entry: "./",
});
});