Release Data Tools 0.2.0
Verify / verify (push) Canceled after 0s

This commit is contained in:
2026-09-02 12:50:51 +02:00
parent e773a2ffe8
commit a8eb8e53b3
36 changed files with 2168 additions and 235 deletions
+27 -1
View File
@@ -109,6 +109,32 @@ test("shows a hardened XML error while retaining the last successful model", asy
).toBeVisible();
});
test("infers a schema and applies a reviewed exact-number edit", async ({
page,
}) => {
await page.goto("/deep/nested/data/");
await expect(page.getByText(/Parsed as JSON/iu)).toBeVisible();
await page.getByRole("tab", { name: /Schema & edit/iu }).click();
await expect(
page.getByRole("heading", { name: "Schema inference" }),
).toBeVisible();
await expect(page.getByRole("rowheader", { name: "/project" })).toBeVisible();
await page.getByLabel(/JSON Pointer/iu).fill("/exactInteger");
await page.getByLabel("Value type").selectOption("number");
await page
.locator(".edit-value-field textarea")
.fill("9007199254740993123456790");
await page.getByRole("button", { name: "Preview edit" }).click();
await expect(
page.getByText("No known loss or coercion in this conversion."),
).toBeVisible();
await page.getByRole("button", { name: "Apply preview to source" }).click();
await expect(page.getByTestId("source-editor")).toHaveValue(
/9007199254740993123456790/u,
);
await expect(page.getByText(/Parsed as JSON/iu)).toBeVisible();
});
test("serves the release identity and hardened headers", async ({
request,
}) => {
@@ -124,7 +150,7 @@ test("serves the release identity and hardened headers", async ({
const manifest = await request.get("/deep/nested/data/toolbox-app.json");
await expect(manifest.json()).resolves.toMatchObject({
id: "de.add-ideas.data-tools",
version: "0.1.0",
version: "0.2.0",
entry: "./",
});
});
+18
View File
@@ -0,0 +1,18 @@
import { expect, test } from "@playwright/test";
test("keeps the primary workspace inside a narrow viewport", async ({
page,
}) => {
await page.goto("/deep/nested/data/");
await expect(page.locator("main").first()).toBeVisible();
await expect(
page.locator("main .loading, main .workbench-loading"),
).toHaveCount(0);
const widths = await page.evaluate(() => ({
content: document.documentElement.scrollWidth,
viewport: document.documentElement.clientWidth,
}));
expect(widths.viewport).toBeLessThanOrEqual(430);
expect(widths.content).toBeLessThanOrEqual(widths.viewport + 1);
});
+2 -2
View File
@@ -9,7 +9,7 @@ async function renderReady() {
vi.fn(async () => new Response("Not found", { status: 404 })),
);
render(<App />);
await screen.findByText(/Parsed as JSON/iu, {}, { timeout: 2_000 });
await screen.findByText(/Parsed as JSON/iu, {}, { timeout: 4_000 });
}
describe("Data Tools workbench", () => {
@@ -31,7 +31,7 @@ describe("Data Tools workbench", () => {
await screen.findByText(
/Unexpected|invalid|property/iu,
{},
{ timeout: 2_000 },
{ timeout: 4_000 },
);
await user.click(screen.getByRole("tab", { name: /Tree/iu }));
expect(screen.getByText("project")).toBeVisible();
+115
View File
@@ -0,0 +1,115 @@
import { describe, expect, it } from "vitest";
import { DATA_ADAPTERS } from "../../src/core/adapters";
import { EXPORT_ADAPTERS, convertDocument } from "../../src/core/convert";
import { applyNodeEdit, parseEditValue } from "../../src/core/edit";
import { normalizeValue } from "../../src/core/model";
import { IMPORT_ADAPTERS, parseDataDocument } from "../../src/core/parse";
import { inferSchema, serializeSchema } from "../../src/core/schema";
import { DATA_FORMATS } from "../../src/core/types";
describe("modular adapters", () => {
it("has an explicit import and export implementation for every descriptor", () => {
expect(DATA_ADAPTERS.map((adapter) => adapter.format)).toEqual(
DATA_FORMATS,
);
expect(Object.keys(IMPORT_ADAPTERS)).toEqual([...DATA_FORMATS]);
expect(Object.keys(EXPORT_ADAPTERS)).toEqual([...DATA_FORMATS]);
});
});
describe("loss-aware schema inference", () => {
it("merges record samples, presence and exact numeric representation", () => {
const document = parseDataDocument({
source:
'{"people":[{"id":9007199254740993123456789,"name":"Ada"},{"id":2,"nickname":null}]}',
format: "json",
});
const inference = inferSchema(document.root);
expect(inference.fields).toEqual(
expect.arrayContaining([
expect.objectContaining({
pointer: "/people/*/id",
required: true,
types: ["integer"],
}),
expect.objectContaining({ pointer: "/people/*/name", required: false }),
expect.objectContaining({
pointer: "/people/*/nickname",
nullable: true,
}),
]),
);
const schema = JSON.parse(serializeSchema(inference)) as Record<
string,
unknown
>;
expect(schema.$schema).toBe("https://json-schema.org/draft/2020-12/schema");
expect(JSON.stringify(schema)).toContain("9007199254740993123456789");
});
it("enforces the field budget across nested objects", () => {
const branch = (prefix: string) =>
Object.fromEntries(
Array.from({ length: 3_000 }, (_, index) => [
`${prefix}${index}`,
index,
]),
);
const inference = inferSchema(
normalizeValue({ left: branch("a"), right: branch("b") }),
);
expect(inference.fields).toHaveLength(5_000);
expect(inference.truncated).toBe(true);
});
});
describe("reviewable structural editing", () => {
const document = parseDataDocument({
source: '{"items":[1,2],"name":"before"}',
format: "json",
});
it("uses strict scalar syntax and immutable RFC 6901 operations", () => {
const replaced = applyNodeEdit(document.root, {
operation: "replace",
pointer: "/name",
value: parseEditValue("string", "after"),
});
const inserted = applyNodeEdit(replaced, {
operation: "add",
pointer: "/items/1",
value: parseEditValue("number", "9007199254740993123456789"),
});
expect(
convertDocument({ ...document, root: inserted }, "json").text,
).toContain("9007199254740993123456789");
expect(convertDocument(document, "json").text).toContain(
'"name": "before"',
);
expect(() => parseEditValue("number", "01")).toThrow(/strict JSON/iu);
expect(() =>
applyNodeEdit(document.root, {
operation: "add",
pointer: "/__proto__",
value: parseEditValue("null", ""),
}),
).toThrow(/prohibited/iu);
});
it("exposes serialization loss before an edit is applied", () => {
const toml = parseDataDocument({
source: "answer = 42\n",
format: "toml",
});
const edited = applyNodeEdit(toml.root, {
operation: "add",
pointer: "/missing",
value: parseEditValue("null", ""),
});
expect(convertDocument({ ...toml, root: edited }, "toml").events).toEqual(
expect.arrayContaining([
expect.objectContaining({ code: "toml.null", severity: "loss" }),
]),
);
});
});