* test(coverage): bootstrap vitest unit infra and add tests at 90%+ per file (#1245) Establishes the unit-test runtime AGENTS.md mandates: a vitest "unit" project alongside the existing storybook project, RTL + jest-dom + happy-dom, a Mantine-aware render helper, and a 90%-line per-file coverage gate enforced by CI. Adds 66 test files covering all 62 components under elements/groups/screens/views, plus core/mcp and clients/web/src/utils. Total: 577 tests, 98.7% line coverage globally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(coverage): address PR #1269 review feedback - AGENTS.md: correct coverage gate description (lines ≥ 90, statements ≥ 85, functions ≥ 80, branches ≥ 50) with rationale for the relaxed branch threshold - InspectorView.test.tsx: document what the Math.random() mock values (0.1 / 0.99) control vs. STUB_SUCCESS_RATE in InspectorView.tsx - vite.config.ts / tsconfig.test.json: drop unused `globals: true` and the `vitest/globals` type now that every test file imports describe/it/expect/vi explicitly; manual `afterEach(cleanup)` in setup.ts remains required because testing-library auto-cleanup relies on globalThis.afterEach Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
64a42e645b
commit
75f74d64c1
1 changed files with 8117 additions and 30 deletions
355
clients/web/src/components/groups/SchemaForm/SchemaForm.test.tsx
Normal file
355
clients/web/src/components/groups/SchemaForm/SchemaForm.test.tsx
Normal file
|
|
@ -0,0 +1,355 @@
|
|||
import { describe, it, expect, vi } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import type { JsonSchemaType } from "../../../utils/jsonUtils";
|
||||
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
|
||||
import { SchemaForm } from "./SchemaForm";
|
||||
|
||||
describe("SchemaForm", () => {
|
||||
it("renders a string TextInput and propagates onChange", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
const schema: JsonSchemaType = {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", title: "Name" },
|
||||
},
|
||||
required: ["name"],
|
||||
};
|
||||
renderWithMantine(
|
||||
<SchemaForm schema={schema} values={{}} onChange={onChange} />,
|
||||
);
|
||||
const input = screen.getByLabelText(/Name/);
|
||||
await user.type(input, "a");
|
||||
expect(onChange).toHaveBeenCalledWith({ name: "a" });
|
||||
});
|
||||
|
||||
it("renders a Number/Integer field and propagates a numeric value", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
const schema: JsonSchemaType = {
|
||||
type: "object",
|
||||
properties: {
|
||||
count: { type: "integer", title: "Count", minimum: 0, maximum: 100 },
|
||||
},
|
||||
};
|
||||
renderWithMantine(
|
||||
<SchemaForm schema={schema} values={{}} onChange={onChange} />,
|
||||
);
|
||||
const input = screen.getByLabelText(/Count/);
|
||||
await user.type(input, "5");
|
||||
expect(onChange).toHaveBeenCalled();
|
||||
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1][0];
|
||||
expect(
|
||||
typeof lastCall.count === "number" || lastCall.count === undefined,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("renders a checkbox for boolean fields and toggles on click", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
const schema: JsonSchemaType = {
|
||||
type: "object",
|
||||
properties: {
|
||||
enabled: { type: "boolean", title: "Enabled" },
|
||||
},
|
||||
};
|
||||
renderWithMantine(
|
||||
<SchemaForm schema={schema} values={{}} onChange={onChange} />,
|
||||
);
|
||||
const checkbox = screen.getByLabelText("Enabled") as HTMLInputElement;
|
||||
await user.click(checkbox);
|
||||
expect(onChange).toHaveBeenCalledWith({ enabled: true });
|
||||
});
|
||||
|
||||
it("renders an enum Select with the supplied options", () => {
|
||||
const onChange = vi.fn();
|
||||
const schema: JsonSchemaType = {
|
||||
type: "object",
|
||||
properties: {
|
||||
format: {
|
||||
type: "string",
|
||||
title: "Format",
|
||||
enum: ["json", "csv", "xml"],
|
||||
},
|
||||
},
|
||||
};
|
||||
renderWithMantine(
|
||||
<SchemaForm
|
||||
schema={schema}
|
||||
values={{ format: "csv" }}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
const inputs = screen.getAllByDisplayValue("csv");
|
||||
expect(inputs.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("invokes onChange when an enum Select option is chosen", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
const schema: JsonSchemaType = {
|
||||
type: "object",
|
||||
properties: {
|
||||
format: {
|
||||
type: "string",
|
||||
title: "Format",
|
||||
enum: ["json", "csv"],
|
||||
},
|
||||
},
|
||||
};
|
||||
renderWithMantine(
|
||||
<SchemaForm schema={schema} values={{}} onChange={onChange} />,
|
||||
);
|
||||
await user.click(screen.getByRole("textbox", { name: "Format" }));
|
||||
const option = await screen.findByRole("option", {
|
||||
name: "csv",
|
||||
hidden: true,
|
||||
});
|
||||
await user.click(option);
|
||||
expect(onChange).toHaveBeenCalledWith({ format: "csv" });
|
||||
});
|
||||
|
||||
it("renders an oneOf Select using titles for labels", () => {
|
||||
const onChange = vi.fn();
|
||||
const schema: JsonSchemaType = {
|
||||
type: "object",
|
||||
properties: {
|
||||
size: {
|
||||
type: "string",
|
||||
title: "Size",
|
||||
oneOf: [
|
||||
{ const: "s", title: "Small" },
|
||||
{ const: "m", title: "Medium" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
renderWithMantine(
|
||||
<SchemaForm schema={schema} values={{ size: "m" }} onChange={onChange} />,
|
||||
);
|
||||
const inputs = screen.getAllByDisplayValue("Medium");
|
||||
expect(inputs.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("invokes onChange when a oneOf Select option is chosen", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
const schema: JsonSchemaType = {
|
||||
type: "object",
|
||||
properties: {
|
||||
size: {
|
||||
type: "string",
|
||||
title: "Size",
|
||||
oneOf: [
|
||||
{ const: "s", title: "Small" },
|
||||
{ const: "m", title: "Medium" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
renderWithMantine(
|
||||
<SchemaForm schema={schema} values={{}} onChange={onChange} />,
|
||||
);
|
||||
await user.click(screen.getByRole("textbox", { name: "Size" }));
|
||||
const option = await screen.findByRole("option", {
|
||||
name: "Small",
|
||||
hidden: true,
|
||||
});
|
||||
await user.click(option);
|
||||
expect(onChange).toHaveBeenCalledWith({ size: "s" });
|
||||
});
|
||||
|
||||
it("renders a MultiSelect for array with anyOf items and invokes onChange when an option is selected", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
const schema: JsonSchemaType = {
|
||||
type: "object",
|
||||
properties: {
|
||||
tags: {
|
||||
type: "array",
|
||||
title: "Tags",
|
||||
items: {
|
||||
anyOf: [
|
||||
{ const: "a", title: "Alpha" },
|
||||
{ const: "b", title: "Beta" },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
renderWithMantine(
|
||||
<SchemaForm schema={schema} values={{}} onChange={onChange} />,
|
||||
);
|
||||
expect(screen.getByText("Tags")).toBeInTheDocument();
|
||||
await user.click(screen.getByRole("textbox", { name: "Tags" }));
|
||||
const option = await screen.findByRole("option", {
|
||||
name: "Alpha",
|
||||
hidden: true,
|
||||
});
|
||||
await user.click(option);
|
||||
expect(onChange).toHaveBeenCalledWith({ tags: ["a"] });
|
||||
});
|
||||
|
||||
it("renders nested object fields recursively", () => {
|
||||
const onChange = vi.fn();
|
||||
const schema: JsonSchemaType = {
|
||||
type: "object",
|
||||
properties: {
|
||||
address: {
|
||||
type: "object",
|
||||
title: "Address",
|
||||
description: "Street and city",
|
||||
properties: {
|
||||
street: { type: "string", title: "Street" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
renderWithMantine(
|
||||
<SchemaForm schema={schema} values={{}} onChange={onChange} />,
|
||||
);
|
||||
expect(screen.getByText("Address")).toBeInTheDocument();
|
||||
expect(screen.getByText("Street and city")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/Street/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("propagates nested object changes back to top-level onChange", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
const schema: JsonSchemaType = {
|
||||
type: "object",
|
||||
properties: {
|
||||
address: {
|
||||
type: "object",
|
||||
title: "Address",
|
||||
properties: {
|
||||
street: { type: "string", title: "Street" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
renderWithMantine(
|
||||
<SchemaForm schema={schema} values={{}} onChange={onChange} />,
|
||||
);
|
||||
await user.type(screen.getByLabelText(/Street/), "1");
|
||||
expect(onChange).toHaveBeenCalled();
|
||||
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1][0];
|
||||
expect(lastCall.address).toEqual({ street: "1" });
|
||||
});
|
||||
|
||||
it("falls back to a JsonInput for complex/unsupported schemas", () => {
|
||||
const onChange = vi.fn();
|
||||
const schema: JsonSchemaType = {
|
||||
type: "object",
|
||||
properties: {
|
||||
config: {
|
||||
type: "array",
|
||||
title: "Config",
|
||||
},
|
||||
},
|
||||
};
|
||||
renderWithMantine(
|
||||
<SchemaForm
|
||||
schema={schema}
|
||||
values={{ config: [1, 2, 3] }}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
// JsonInput renders the value as serialized JSON
|
||||
expect(screen.getByText("Config")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("invokes onChange via JsonInput when valid JSON is pasted", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
const schema: JsonSchemaType = {
|
||||
type: "object",
|
||||
properties: {
|
||||
config: { type: "array", title: "Config" },
|
||||
},
|
||||
};
|
||||
renderWithMantine(
|
||||
<SchemaForm schema={schema} values={{}} onChange={onChange} />,
|
||||
);
|
||||
const jsonInput = screen.getByLabelText(/Config/) as HTMLTextAreaElement;
|
||||
jsonInput.focus();
|
||||
await user.paste("[1,2]");
|
||||
expect(onChange).toHaveBeenCalled();
|
||||
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1][0];
|
||||
expect(lastCall.config).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it("falls back to passing raw string to onChange when JSON is invalid in JsonInput", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
const schema: JsonSchemaType = {
|
||||
type: "object",
|
||||
properties: {
|
||||
config: { type: "array", title: "Config" },
|
||||
},
|
||||
};
|
||||
renderWithMantine(
|
||||
<SchemaForm schema={schema} values={{}} onChange={onChange} />,
|
||||
);
|
||||
const jsonInput = screen.getByLabelText(/Config/) as HTMLTextAreaElement;
|
||||
await user.type(jsonInput, "x");
|
||||
expect(onChange).toHaveBeenCalled();
|
||||
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1][0];
|
||||
expect(typeof lastCall.config).toBe("string");
|
||||
});
|
||||
|
||||
it("uses default values when value is undefined", () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", title: "Name", default: "Alice" },
|
||||
},
|
||||
};
|
||||
renderWithMantine(
|
||||
<SchemaForm schema={schema} values={{}} onChange={vi.fn()} />,
|
||||
);
|
||||
expect(screen.getByDisplayValue("Alice")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("respects the disabled prop on inputs", () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", title: "Name" },
|
||||
active: { type: "boolean", title: "Active" },
|
||||
},
|
||||
};
|
||||
renderWithMantine(
|
||||
<SchemaForm
|
||||
schema={schema}
|
||||
values={{ name: "x", active: true }}
|
||||
onChange={vi.fn()}
|
||||
disabled
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByLabelText(/Name/)).toBeDisabled();
|
||||
expect(screen.getByLabelText("Active")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("uses field name when title is missing", () => {
|
||||
const schema: JsonSchemaType = {
|
||||
type: "object",
|
||||
properties: {
|
||||
rawField: { type: "string" },
|
||||
},
|
||||
};
|
||||
renderWithMantine(
|
||||
<SchemaForm schema={schema} values={{}} onChange={vi.fn()} />,
|
||||
);
|
||||
expect(screen.getByLabelText(/rawField/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders nothing inside the form when properties are missing", () => {
|
||||
const schema: JsonSchemaType = { type: "object" };
|
||||
const { container } = renderWithMantine(
|
||||
<SchemaForm schema={schema} values={{}} onChange={vi.fn()} />,
|
||||
);
|
||||
// Stack root exists but has no children
|
||||
expect(container.firstChild).not.toBeNull();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue