Release Random Tools 0.1.1

This commit is contained in:
2026-09-01 08:27:00 +02:00
parent 2ed2560b95
commit e4aa85a503
28 changed files with 1072 additions and 282 deletions
+95 -3
View File
@@ -28,7 +28,7 @@ test("runs from a nested path without external requests", async ({ page }) => {
expect(errors).toEqual([]);
});
test("repeats deterministic output without contacting the remote source", async ({
test("repeats deterministic output without external requests", async ({
page,
}) => {
const external = await localOnly(page);
@@ -50,6 +50,95 @@ test("repeats deterministic output without contacting the remote source", async
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,
}) => {
@@ -59,13 +148,16 @@ test("serves the release identity and hardened headers", async ({
"default-src 'self'",
);
expect(index.headers()["content-security-policy"]).toContain(
"connect-src 'self' https://www.random.org",
"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.0",
version: "0.1.1",
entry: "./",
});
});
+3 -1
View File
@@ -12,6 +12,8 @@ describe("Random Tools", () => {
expect(
await screen.findByRole("heading", { name: "Random Tools" }),
).toBeVisible();
expect(await screen.findByText("WebCrypto by default")).toBeVisible();
expect(await screen.findByText("No network requests")).toBeVisible();
expect(screen.getByRole("tab", { name: "Draws" })).toBeVisible();
expect(screen.queryByText(/RANDOM\.ORG/iu)).not.toBeInTheDocument();
});
});
+153
View File
@@ -1,6 +1,9 @@
import { describe, expect, it } from "vitest";
import {
normalizeWordList,
passphrase,
passphraseWithWordList,
randomIdentifiers,
randomIntegers,
randomString,
rollDice,
@@ -8,6 +11,14 @@ import {
uuidV4,
uuidV7,
} from "../../src/random/generators";
import {
coinFlips,
dealCards,
decimalFractions,
integerSequence,
randomCoordinates,
randomDates,
} from "../../src/random/draws";
import { randomSource } from "../../src/random/source";
describe("random generators", () => {
@@ -28,6 +39,38 @@ describe("random generators", () => {
expect(ulid(randomSource("deterministic", "id"), 0)).toMatch(
/^[0-9A-HJKMNP-TV-Z]{26}$/u,
));
it("bounds identifier batches before allocating their output array", () => {
const source = randomSource("deterministic", "identifier-bounds");
expect(() => randomIdentifiers(source, 0, "uuid4", 0)).toThrow(
/safe integer from 1100,000/u,
);
expect(() => randomIdentifiers(source, 100_001, "uuid7", 0)).toThrow(
/safe integer from 1100,000/u,
);
expect(() => randomIdentifiers(source, Number.NaN, "ulid", 0)).toThrow(
/safe integer from 1100,000/u,
);
expect(randomIdentifiers(source, 2, "uuid4", 0)).toHaveLength(2);
});
it("rejects an inclusive MAX_SAFE_INTEGER bound before adding one", () => {
const source = randomSource("deterministic", "integer-boundary");
expect(() =>
randomIntegers(
source,
1,
Number.MAX_SAFE_INTEGER,
Number.MAX_SAFE_INTEGER,
),
).toThrow(/less than Number\.MAX_SAFE_INTEGER/u);
expect(
randomIntegers(
source,
1,
Number.MAX_SAFE_INTEGER - 1,
Number.MAX_SAFE_INTEGER - 1,
),
).toEqual([Number.MAX_SAFE_INTEGER - 1]);
});
it("rejects biased duplicate alphabets", () =>
expect(() =>
randomString(randomSource("deterministic", "x"), 3, "aab"),
@@ -45,6 +88,21 @@ describe("random generators", () => {
"four",
]).entropy,
).toBe(8));
it("normalizes a custom word list once and reports its eligible count", () => {
const words = normalizeWordList(" one \r\n\r\ntwo\n three ");
expect(words).toMatchObject({
words: ["one", "two", "three"],
canonical: "one\ntwo\nthree",
normalization: "trim-lines-drop-empty-preserve-order-v1",
});
expect(
passphraseWithWordList(
randomSource("deterministic", "normalized-words"),
4,
words,
),
).toMatchObject({ listSize: 3, entropy: 4 * Math.log2(3) });
});
it("bounds alphabet and word-list allocation before generation", () => {
const source = randomSource("deterministic", "bounds");
expect(() => randomString(source, 1, "ab".repeat(70_000))).toThrow(
@@ -55,3 +113,98 @@ describe("random generators", () => {
);
});
});
describe("local draws", () => {
it("reproduces coin flips and reports only binary outcomes", () => {
const first = coinFlips(randomSource("deterministic", "coins"), 50);
const second = coinFlips(randomSource("deterministic", "coins"), 50);
expect(first).toEqual(second);
expect(first.every((value) => value === "Heads" || value === "Tails")).toBe(
true,
);
});
it("deals every card from a standard deck without replacement", () => {
const cards = dealCards(randomSource("deterministic", "cards"), 1, 52);
expect(cards).toHaveLength(52);
expect(new Set(cards).size).toBe(52);
expect(
cards.every((card) => /^(?:A|[2-9]|10|J|Q|K)[]$/u.test(card)),
).toBe(true);
});
it("shuffles an inclusive integer range exactly once", () => {
const values = integerSequence(
randomSource("deterministic", "sequence"),
-2,
3,
);
expect(values).toHaveLength(6);
expect([...values].sort((a, b) => a - b)).toEqual([-2, -1, 0, 1, 2, 3]);
});
it("filters dates by weekday and can sample without replacement", () => {
const values = randomDates(
randomSource("deterministic", "dates"),
4,
"2024-02-01",
"2024-02-29",
{ weekdays: [1], withoutReplacement: true },
);
expect(values).toHaveLength(4);
expect(new Set(values).size).toBe(4);
expect(
values.every((value) => new Date(`${value}T00:00:00Z`).getUTCDay() === 1),
).toBe(true);
expect(() =>
randomDates(
randomSource("deterministic", "bad-date"),
1,
"2023-02-29",
"2023-03-01",
),
).toThrow(/valid Gregorian/u);
});
it("generates exact-length decimal fractions with no binary formatting", () => {
const values = decimalFractions(
randomSource("deterministic", "fractions"),
4,
12,
);
expect(values).toHaveLength(4);
expect(values.every((value) => /^0\.\d{12}$/u.test(value))).toBe(true);
});
it("generates reproducible coordinates inside geographic bounds", () => {
const first = randomCoordinates(
randomSource("deterministic", "coordinates"),
100,
6,
);
const second = randomCoordinates(
randomSource("deterministic", "coordinates"),
100,
6,
);
expect(first).toEqual(second);
expect(
first.every(
({ latitude, longitude }) =>
latitude >= -90 &&
latitude <= 90 &&
longitude >= -180 &&
longitude <= 180,
),
).toBe(true);
});
it("rejects draw requests beyond their allocation bounds", () => {
const source = randomSource("deterministic", "bounds");
expect(() => coinFlips(source, 100_001)).toThrow(/100,000/u);
expect(() => dealCards(source, 1, 53)).toThrow(/152/u);
expect(() => integerSequence(source, 0, 100_000)).toThrow(/100,000/u);
expect(() => decimalFractions(source, 100_000, 64)).toThrow(/4,000,000/u);
expect(() => randomCoordinates(source, 1, 11)).toThrow(/010/u);
});
});