Release Random Tools 0.1.1
This commit is contained in:
@@ -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: "./",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 1–100,000/u,
|
||||
);
|
||||
expect(() => randomIdentifiers(source, 100_001, "uuid7", 0)).toThrow(
|
||||
/safe integer from 1–100,000/u,
|
||||
);
|
||||
expect(() => randomIdentifiers(source, Number.NaN, "ulid", 0)).toThrow(
|
||||
/safe integer from 1–100,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(/1–52/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(/0–10/u);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user