diff --git a/packages/chess/src/ui/ParamField.snapshot.test.tsx b/packages/chess/src/ui/ParamField.snapshot.test.tsx index 3bfd086..ce04014 100644 --- a/packages/chess/src/ui/ParamField.snapshot.test.tsx +++ b/packages/chess/src/ui/ParamField.snapshot.test.tsx @@ -145,21 +145,19 @@ describe("ParamField rendering (T14 regression baseline)", () => { }); /** * `block-move-type` and `override-promotion` use `z.enum([...])`. - * The inspector's current introspection reads `_def.values`, which - * is `undefined` on Zod v4's ZodEnum (shape renamed to `entries`). - * That means the inline component throws before rendering for these - * kinds — a pre-existing behaviour the pure refactor must preserve. - * The assertion below pins the throw so the extracted `ParamField` - * continues to reproduce it exactly. + * Previously the inspector read `_def.values`, which is `undefined` + * on Zod v4's ZodEnum (renamed to `entries`), causing a render + * crash. ParamField now reads `.options` / `_def.entries` and + * renders cleanly for both kinds. */ - it("block-move-type throws today (pre-existing z.enum bug)", () => { - expect(() => render("block-move-type")).toThrow(); + it("block-move-type renders moveType enum", () => { + expect(render("block-move-type")).toMatchSnapshot(); }); it("modify-movement-range renders delta", () => { expect(render("modify-movement-range")).toMatchSnapshot(); }); - it("override-promotion throws today (pre-existing z.enum bug)", () => { - expect(() => render("override-promotion")).toThrow(); + it("override-promotion renders target enum", () => { + expect(render("override-promotion")).toMatchSnapshot(); }); it("add-aura renders radius + targetAttr + delta", () => { expect(render("add-aura")).toMatchSnapshot(); diff --git a/packages/chess/src/ui/ParamField.tsx b/packages/chess/src/ui/ParamField.tsx index 28fb380..dad9953 100644 --- a/packages/chess/src/ui/ParamField.tsx +++ b/packages/chess/src/ui/ParamField.tsx @@ -219,7 +219,15 @@ export function ParamField({ type ZodWrappedDefInternal = { _def: { innerType?: ZodType; schema?: ZodType }; }; - type ZodEnumDefInternal = { _def: { values: readonly string[] } }; + /** + * Zod v4 renamed `_def.values` to `_def.entries` (a `Record`) and also exposes the option list as `.options`. We read + * whichever is populated, preferring the public `options` array. + */ + type ZodEnumDefInternal = { + options?: readonly string[]; + _def: { values?: readonly string[]; entries?: Record }; + }; const shape = (primitive.paramsSchema as unknown as ZodObjectInternal).shape; const params = (node.params as Record) || {}; @@ -246,7 +254,12 @@ export function ParamField({ else if (currentSchema instanceof z.ZodBoolean) type = 'boolean'; else if (currentSchema instanceof z.ZodEnum) { type = 'enum'; - options = (currentSchema as unknown as ZodEnumDefInternal)._def.values; + const enumSchema = currentSchema as unknown as ZodEnumDefInternal; + options = + enumSchema.options ?? + (enumSchema._def.entries + ? (Object.values(enumSchema._def.entries) as readonly string[]) + : enumSchema._def.values ?? []); } else if (currentSchema instanceof z.ZodArray) type = 'array'; diff --git a/packages/chess/src/ui/__snapshots__/ParamField.snapshot.test.tsx.snap b/packages/chess/src/ui/__snapshots__/ParamField.snapshot.test.tsx.snap index 9579c1e..6fa055a 100644 --- a/packages/chess/src/ui/__snapshots__/ParamField.snapshot.test.tsx.snap +++ b/packages/chess/src/ui/__snapshots__/ParamField.snapshot.test.tsx.snap @@ -50,6 +50,14 @@ exports[`ParamField rendering (T14 regression baseline) > add-to-attribute rende }

Wrapped in on-turn-start, restores 1 HP to this piece at the start of its color's turn.

" `; +exports[`ParamField rendering (T14 regression baseline) > block-move-type renders moveType enum 1`] = ` +"

Filters out generated moves matching the given type. Multiple block primitives accumulate into a blocked-move-type set (deduped). Useful for pacifist pieces that still slide, or for pieces that can capture but not reposition silently.

Examples
Pacifist piece
{
+  "moveType": "capture"
+}

Piece can step and slide freely but cannot capture — a pure support piece.

Charge-only attacker
{
+  "moveType": "step"
+}

Removes simple step moves; piece can only capture or slide.

" +`; + exports[`ParamField rendering (T14 regression baseline) > conditional renders complex-schema JSON fallback 1`] = ` "

Branches on a condition. If true → runs every primitive in \`then\`; if false and \`else\` is set → runs \`else\`. Condition types: attr-lt (numeric less-than), attr-gt (numeric greater-than), attr-eq (exact match against string/number/boolean/null), always (unconditional then), never (forces else path only).

Examples
Low-HP fortress
{
   "condition": {
@@ -169,6 +177,14 @@ exports[`ParamField rendering (T14 regression baseline) > on-turn-start renders
 ]
" `; +exports[`ParamField rendering (T14 regression baseline) > override-promotion renders target enum 1`] = ` +"

Forces this piece (typically a pawn) to promote to a specific type regardless of player choice. Mirrors the built-in Promotion Override modifier, but expressable inside a custom primitive tree. Last write wins if multiple sources set it.

Examples
Knights-only promotion
{
+  "target": "knight"
+}

Pawn always promotes to a knight.

Underpromote to rook
{
+  "target": "rook"
+}

Pawn always promotes to a rook — useful for themed variants.

" +`; + exports[`ParamField rendering (T14 regression baseline) > reflect-damage renders percentage 1`] = ` "

Reflects a percentage of incoming damage back to the attacker. Integer percent, 0-100. Multiple reflect primitives on the same piece do NOT stack — the most recent value wins. Great inside on-damaged if you want a one-time thorns reaction instead of a permanent aura.

Examples
Half-reflective armour
{
   "percentage": 50