diff --git a/packages/mcp/src/tools/validate.ts b/packages/mcp/src/tools/validate.ts index 87475d9..a02dd63 100644 --- a/packages/mcp/src/tools/validate.ts +++ b/packages/mcp/src/tools/validate.ts @@ -3,6 +3,11 @@ import type { ComponentRegistry } from "../registry.js"; interface ValidateInput { component: string; schema?: string; props?: Record; parts?: string[]; } interface ValidateResult { valid: boolean; errors: string[]; } +interface ZodParseIssue { message: string; path: (string | number)[]; } +interface ZodParseError { issues: ZodParseIssue[]; } +interface ZodParseResult { success: boolean; error?: ZodParseError; } +interface ZodSchema { safeParse(data: unknown): ZodParseResult; } + /** Validates props against schema and/or checks required parts are present. */ export function handleValidate(registry: ComponentRegistry, input: ValidateInput): ValidateResult { const comp = registry.getComponent(input.component); @@ -16,8 +21,7 @@ export function handleValidate(registry: ComponentRegistry, input: ValidateInput if (!schema) { errors.push(`Schema "${input.schema}" not found on ${comp.meta.name}`); } else { - // schema is a Zod schema — call safeParse - const zSchema = schema as { safeParse: (data: unknown) => { success: boolean; error?: { issues: Array<{ message: string; path: (string | number)[] }> } } }; + const zSchema = schema as ZodSchema; const result = zSchema.safeParse(input.props); if (!result.success && result.error) { for (const issue of result.error.issues) { diff --git a/packages/mcp/tests/tools/validate.test.ts b/packages/mcp/tests/tools/validate.test.ts new file mode 100644 index 0000000..6288394 --- /dev/null +++ b/packages/mcp/tests/tools/validate.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect } from "vitest"; +import { handleValidate } from "../../src/tools/validate.js"; +import { ComponentRegistry } from "../../src/registry.js"; + +describe("pettyui.validate", () => { + const registry = new ComponentRegistry(); + + it("validates correct props", () => { + const result = handleValidate(registry, { component: "Dialog", schema: "dialogRoot", props: { open: true, modal: false } }); + expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it("rejects invalid props", () => { + const result = handleValidate(registry, { component: "Dialog", schema: "dialogRoot", props: { open: "yes" } }); + expect(result.valid).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + }); + + it("validates empty props", () => { + const result = handleValidate(registry, { component: "Dialog", schema: "dialogRoot", props: {} }); + expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it("checks required parts present — missing Title", () => { + const result = handleValidate(registry, { component: "Dialog", parts: ["Root", "Content"] }); + expect(result.valid).toBe(false); + expect(result.errors).toContain("Missing required part: Title"); + }); + + it("passes when all required parts provided", () => { + const result = handleValidate(registry, { component: "Dialog", parts: ["Root", "Content", "Title"] }); + expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it("returns error for unknown component", () => { + const result = handleValidate(registry, { component: "FakeComponent" }); + expect(result.valid).toBe(false); + expect(result.errors).toContain('Component "FakeComponent" not found'); + }); +});