Release Fixture Tools 0.1.0
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
|
||||
const ORIGIN = "http://127.0.0.1:4194";
|
||||
async function watchLocalOnly(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 at a nested path and generates deterministic relational data", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 1800, height: 1000 });
|
||||
const external = await watchLocalOnly(page);
|
||||
await page.goto("/deep/nested/fixture/");
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Fixture Tools" }),
|
||||
).toBeVisible();
|
||||
await page.getByRole("button", { name: "Generate fixtures" }).click();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Generated records" }),
|
||||
).toBeVisible();
|
||||
await page.getByLabel("Data table").selectOption("orders");
|
||||
await expect(
|
||||
page.getByRole("columnheader", { name: "user_id" }),
|
||||
).toBeVisible();
|
||||
expect(external).toEqual([]);
|
||||
expect(
|
||||
await page
|
||||
.locator(".toolbox-shell__main")
|
||||
.evaluate((node) => getComputedStyle(node).width),
|
||||
).toBe("1440px");
|
||||
});
|
||||
|
||||
test("imports CSV headings and exports an inspectable model", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/deep/nested/fixture/");
|
||||
await page.getByLabel("Source kind").selectOption("csv");
|
||||
await page.getByLabel("Fixture model source").fill("id,email,active\n");
|
||||
await page.getByRole("button", { name: "Import model" }).click();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Editable field model" }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByLabel("email type")).toHaveValue("email");
|
||||
const pending = page.waitForEvent("download");
|
||||
await page.getByRole("button", { name: "Export model" }).click();
|
||||
expect((await pending).suggestedFilename()).toBe("fixture-model.json");
|
||||
});
|
||||
|
||||
test("retains results after a failed import and downloads output", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/deep/nested/fixture/");
|
||||
await page.getByLabel("Source kind").selectOption("json-schema");
|
||||
await page.getByLabel("Fixture model source").fill("{");
|
||||
await page.getByRole("button", { name: "Import model" }).click();
|
||||
await expect(page.getByRole("alert")).toContainText("retained");
|
||||
await page.getByRole("button", { name: "Data" }).click();
|
||||
const pending = page.waitForEvent("download");
|
||||
await page.getByRole("button", { name: "Download JSON" }).click();
|
||||
expect((await pending).suggestedFilename()).toBe("fixtures.json");
|
||||
});
|
||||
|
||||
test("integrates help, themes, PWA identity and hardened headers", async ({
|
||||
page,
|
||||
request,
|
||||
}) => {
|
||||
await page.goto("/deep/nested/fixture/");
|
||||
await page.getByRole("button", { name: "Help" }).click();
|
||||
await expect(
|
||||
page.getByRole("dialog", { name: "About Fixture Tools" }),
|
||||
).toContainText("synthetic");
|
||||
await page.keyboard.press("Escape");
|
||||
await page.getByRole("button", { name: "Personalize" }).click();
|
||||
await page.getByRole("button", { name: "Dark" }).click();
|
||||
await expect(page.locator(".toolbox-shell").first()).toHaveAttribute(
|
||||
"data-toolbox-theme",
|
||||
"dark",
|
||||
);
|
||||
expect(
|
||||
await page.evaluate(async () =>
|
||||
Boolean(await navigator.serviceWorker.ready),
|
||||
),
|
||||
).toBe(true);
|
||||
const index = await request.get("/deep/nested/fixture/");
|
||||
expect(index.headers()["content-security-policy"]).toContain(
|
||||
"connect-src 'self'",
|
||||
);
|
||||
expect(await index.text()).not.toMatch(/\b(?:src|href)=["']\//u);
|
||||
const manifest = await request.get("/deep/nested/fixture/toolbox-app.json");
|
||||
await expect(manifest.json()).resolves.toMatchObject({
|
||||
id: "de.add-ideas.fixture-tools",
|
||||
version: "0.1.0",
|
||||
privacy: { processing: "local", telemetry: false },
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Workbench } from "../../src/components/Workbench";
|
||||
|
||||
describe("Fixture workbench", () => {
|
||||
it("generates records and exposes intentional invalid cases", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Workbench />);
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Fixture Tools" }),
|
||||
).toBeInTheDocument();
|
||||
const invalid = screen.getByLabelText("Intentional invalids (%)");
|
||||
await user.clear(invalid);
|
||||
await user.type(invalid, "100");
|
||||
await user.click(screen.getByRole("button", { name: "Generate fixtures" }));
|
||||
await user.click(screen.getByRole("button", { name: /Invalid cases/u }));
|
||||
expect(
|
||||
screen.getAllByText(/required|unique|foreignKey/u).length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("retains the active model after malformed input", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Workbench />);
|
||||
await user.selectOptions(
|
||||
screen.getByLabelText("Source kind"),
|
||||
"json-schema",
|
||||
);
|
||||
const source = screen.getByLabelText("Fixture model source");
|
||||
await user.clear(source);
|
||||
await user.type(source, "{{");
|
||||
await user.click(screen.getByRole("button", { name: "Import model" }));
|
||||
expect(screen.getByRole("alert")).toHaveTextContent(/retained/u);
|
||||
expect(screen.getByLabelText("Data table")).toHaveValue("users");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,178 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
generateFixtures,
|
||||
modelJson,
|
||||
parseFixtureModel,
|
||||
serializeDataset,
|
||||
type GeneratedDataset,
|
||||
} from "../../src/fixture/model";
|
||||
|
||||
const SQL = `CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
email VARCHAR(80) NOT NULL UNIQUE,
|
||||
score DECIMAL(5,2)
|
||||
);
|
||||
CREATE TABLE orders (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id)
|
||||
);`;
|
||||
|
||||
describe("fixture model", () => {
|
||||
it("imports focused SQL constraints and preserves foreign keys", () => {
|
||||
const parsed = parseFixtureModel(SQL, "sql");
|
||||
expect(parsed.model.tables).toHaveLength(2);
|
||||
expect(parsed.model.tables[0]?.fields[0]).toMatchObject({
|
||||
name: "id",
|
||||
type: "integer",
|
||||
required: true,
|
||||
unique: true,
|
||||
distribution: "sequential",
|
||||
});
|
||||
expect(parsed.model.tables[0]?.fields[1]).toMatchObject({
|
||||
name: "email",
|
||||
type: "email",
|
||||
unique: true,
|
||||
maxLength: 80,
|
||||
});
|
||||
expect(parsed.model.tables[1]?.fields[1]).toMatchObject({
|
||||
type: "foreignKey",
|
||||
reference: "users.id",
|
||||
});
|
||||
});
|
||||
|
||||
it("generates replayable records with referential integrity", () => {
|
||||
const model = parseFixtureModel(SQL, "sql").model;
|
||||
const options = {
|
||||
seed: "repeat-me",
|
||||
rows: 20,
|
||||
boundaryPercent: 15,
|
||||
invalidPercent: 0,
|
||||
};
|
||||
const first = generateFixtures(model, options);
|
||||
const second = generateFixtures(model, options);
|
||||
expect(first).toEqual(second);
|
||||
expect(
|
||||
generateFixtures(model, { ...options, seed: "different" }),
|
||||
).not.toEqual(first);
|
||||
const ids = new Set(first.tables.users?.map((row) => row.id));
|
||||
expect(first.tables.orders?.every((row) => ids.has(row.user_id))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(new Set(first.tables.users?.map((row) => row.email)).size).toBe(20);
|
||||
});
|
||||
|
||||
it("imports JSON Schema bounds and round-trips the editable model", () => {
|
||||
const parsed = parseFixtureModel(
|
||||
JSON.stringify({
|
||||
title: "People",
|
||||
type: "object",
|
||||
required: ["age"],
|
||||
properties: {
|
||||
age: { type: "integer", minimum: 18, maximum: 90 },
|
||||
state: { enum: ["draft", "active"] },
|
||||
nickname: { type: "string", minLength: 2, maxLength: 8 },
|
||||
},
|
||||
}),
|
||||
"json-schema",
|
||||
);
|
||||
expect(parsed.model.tables[0]).toMatchObject({ name: "People" });
|
||||
expect(parsed.model.tables[0]?.fields[0]).toMatchObject({
|
||||
minimum: 18,
|
||||
maximum: 90,
|
||||
required: true,
|
||||
});
|
||||
expect(
|
||||
parseFixtureModel(modelJson(parsed.model), "fixture-model").model,
|
||||
).toEqual(parsed.model);
|
||||
});
|
||||
|
||||
it("records every intentional mutation and emits common formats", () => {
|
||||
const model = parseFixtureModel("id,email\n", "csv").model;
|
||||
const dataset = generateFixtures(model, {
|
||||
seed: "invalid",
|
||||
rows: 3,
|
||||
boundaryPercent: 0,
|
||||
invalidPercent: 100,
|
||||
});
|
||||
expect(dataset.violations).toHaveLength(6);
|
||||
expect(serializeDataset(dataset, "json")).toMatch(/^\[/u);
|
||||
expect(serializeDataset(dataset, "ndjson").trim().split("\n")).toHaveLength(
|
||||
3,
|
||||
);
|
||||
expect(serializeDataset(dataset, "xml")).toContain("<fixtures");
|
||||
expect(serializeDataset(dataset, "sql")).toContain('INSERT INTO "records"');
|
||||
});
|
||||
|
||||
it("makes CSV spreadsheet-safe and quotes SQL literals", () => {
|
||||
const dataset: GeneratedDataset = {
|
||||
seed: "safe",
|
||||
rowsPerTable: 1,
|
||||
tables: {
|
||||
records: [{ value: '=HYPERLINK("https://invalid")', quote: "O'Brien" }],
|
||||
},
|
||||
violations: [],
|
||||
};
|
||||
expect(serializeDataset(dataset, "csv")).toContain("'=HYPERLINK");
|
||||
expect(serializeDataset(dataset, "sql")).toContain("O''Brien");
|
||||
});
|
||||
|
||||
it("imports bounded XSD and rejects active document declarations", () => {
|
||||
const xsd = `<?xml version="1.0"?><xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"><xs:element name="people"><xs:complexType><xs:sequence><xs:element name="age" type="xs:integer"/><xs:element name="email" type="xs:string" minOccurs="0"/></xs:sequence></xs:complexType></xs:element></xs:schema>`;
|
||||
const parsed = parseFixtureModel(xsd, "xsd");
|
||||
expect(parsed.model.tables[0]?.fields).toMatchObject([
|
||||
{ name: "age", type: "integer", required: true },
|
||||
{ name: "email", required: false },
|
||||
]);
|
||||
expect(() =>
|
||||
parseFixtureModel('<!DOCTYPE x [<!ENTITY y "z">]><x/>', "xsd"),
|
||||
).toThrow(/DOCTYPE/u);
|
||||
});
|
||||
|
||||
it("rejects misleading recipes and caps wide generations", () => {
|
||||
expect(() =>
|
||||
parseFixtureModel(
|
||||
JSON.stringify({
|
||||
tables: [
|
||||
{
|
||||
name: "records",
|
||||
fields: [
|
||||
{
|
||||
name: "id",
|
||||
type: "integer",
|
||||
required: true,
|
||||
unique: true,
|
||||
distribution: "constant",
|
||||
constant: "wrong type",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
"fixture-model",
|
||||
),
|
||||
).toThrow(/constant/u);
|
||||
|
||||
const wide = {
|
||||
tables: [
|
||||
{
|
||||
name: "wide",
|
||||
fields: Array.from({ length: 100 }, (_unused, index) => ({
|
||||
name: `field_${index}`,
|
||||
type: "string" as const,
|
||||
required: true,
|
||||
unique: false,
|
||||
distribution: "sequential" as const,
|
||||
})),
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(() =>
|
||||
generateFixtures(wide, {
|
||||
seed: "bounded",
|
||||
rows: 1_001,
|
||||
boundaryPercent: 0,
|
||||
invalidPercent: 0,
|
||||
}),
|
||||
).toThrow(/100,000 field values/u);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user