From 2bc8d19c333d4e9678955d21bddec768a726ad4a Mon Sep 17 00:00:00 2001 From: Jiho Lee Date: Sun, 26 Jul 2026 01:29:31 +0900 Subject: [PATCH] fix: omit group anchor_alignment and subcircuit_id instead of writing null (#2845) --- .../primitive-components/Group/Group.ts | 9 ++- .../group-circuit-json-nulls.test.tsx | 63 +++++++++++++++++++ .../group-subcircuit-id.test.tsx | 2 +- tests/groups/group-outline.test.tsx | 2 +- tests/groups/group-schematic-box.test.tsx | 2 +- 5 files changed, 73 insertions(+), 5 deletions(-) create mode 100644 tests/components/primitive-components/group-circuit-json-nulls.test.tsx diff --git a/lib/components/primitive-components/Group/Group.ts b/lib/components/primitive-components/Group/Group.ts index 7e902c6ab..0db9d828e 100644 --- a/lib/components/primitive-components/Group/Group.ts +++ b/lib/components/primitive-components/Group/Group.ts @@ -465,7 +465,9 @@ export class Group = typeof groupProps> trace_clearance: props.autorouter.traceClearance, } : undefined, - anchor_alignment: props.pcbAnchorAlignment ?? null, + // circuit-json declares this as `ninePointAnchor.default("center")`, so a + // `null` fails validation while omitting it lets the schema default apply. + anchor_alignment: props.pcbAnchorAlignment ?? undefined, }) this.pcb_group_id = pcb_group.pcb_group_id @@ -1587,7 +1589,10 @@ export class Group = typeof groupProps> const { _parsedProps: props } = this const schematic_group = db.schematic_group.insert({ is_subcircuit: this.isSubcircuit, - subcircuit_id: this.subcircuit_id!, + // `subcircuit_id` is `string | null` on the component but + // `z.string().optional()` on the schema, so `null` fails validation. + // A group outside any subcircuit simply has none. + subcircuit_id: this.subcircuit_id ?? undefined, name: this.name, center: this._getGlobalSchematicPositionBeforeLayout(), width: 0, diff --git a/tests/components/primitive-components/group-circuit-json-nulls.test.tsx b/tests/components/primitive-components/group-circuit-json-nulls.test.tsx new file mode 100644 index 000000000..42d27985f --- /dev/null +++ b/tests/components/primitive-components/group-circuit-json-nulls.test.tsx @@ -0,0 +1,63 @@ +import { expect, test } from "bun:test" +import * as CJ from "circuit-json" +import { getTestFixture } from "tests/fixtures/get-test-fixture" + +test("groups do not emit null anchor_alignment / subcircuit_id", async () => { + const { circuit } = getTestFixture() + + circuit.add( + + + + + + + + , + ) + + await circuit.renderUntilSettled() + + const circuitJson = circuit.getCircuitJson() + const pcbGroups = circuitJson.filter( + (e: any) => e.type === "pcb_group", + ) as any[] + const schematicGroups = circuitJson.filter( + (e: any) => e.type === "schematic_group", + ) as any[] + + expect(pcbGroups.length).toBeGreaterThan(0) + expect(schematicGroups.length).toBeGreaterThan(0) + + // `anchor_alignment` is `ninePointAnchor.default("center")` — `null` fails + // validation, while omitting it lets the schema default apply. + for (const group of pcbGroups) { + expect(group.anchor_alignment).not.toBeNull() + } + + // `subcircuit_id` is `z.string().optional()`, so `null` is rejected. + for (const group of schematicGroups) { + expect(group.subcircuit_id).not.toBeNull() + } + + // An explicitly requested alignment must still survive. + const explicit = pcbGroups.find((g) => g.anchor_alignment === "top_left") + expect(explicit).toBeDefined() + + // And these two fields must no longer be the reason validation fails. + for (const group of [...pcbGroups, ...schematicGroups]) { + const schema = (CJ as any)[group.type] + const result = schema.safeParse(group) + const relevantIssues = result.success + ? [] + : result.error.issues + .filter((i: any) => + ["anchor_alignment", "subcircuit_id"].includes(String(i.path[0])), + ) + .map((i: any) => `${i.path.join(".")}: ${i.message}`) + expect({ type: group.type, relevantIssues }).toEqual({ + type: group.type, + relevantIssues: [], + }) + } +}) diff --git a/tests/components/primitive-components/group-subcircuit-id.test.tsx b/tests/components/primitive-components/group-subcircuit-id.test.tsx index 5aa093fac..c92e506d4 100644 --- a/tests/components/primitive-components/group-subcircuit-id.test.tsx +++ b/tests/components/primitive-components/group-subcircuit-id.test.tsx @@ -34,7 +34,7 @@ test("Subcircuit group should have subcircuit_id", async () => { expect(circuit.db.pcb_group.list()).toMatchInlineSnapshot(` [ { - "anchor_alignment": null, + "anchor_alignment": undefined, "anchor_position": { "x": 0, "y": 0, diff --git a/tests/groups/group-outline.test.tsx b/tests/groups/group-outline.test.tsx index cc12b8760..22c45b4ae 100644 --- a/tests/groups/group-outline.test.tsx +++ b/tests/groups/group-outline.test.tsx @@ -26,7 +26,7 @@ test("group with outline specified", async () => { expect(pcb_groups).toMatchInlineSnapshot(` [ { - "anchor_alignment": null, + "anchor_alignment": undefined, "anchor_position": { "x": 0, "y": 0, diff --git a/tests/groups/group-schematic-box.test.tsx b/tests/groups/group-schematic-box.test.tsx index 027d3f568..32a812b15 100644 --- a/tests/groups/group-schematic-box.test.tsx +++ b/tests/groups/group-schematic-box.test.tsx @@ -33,7 +33,7 @@ test("group schematic box", () => { "schematic_group_id": "schematic_group_0", "show_as_schematic_box": true, "source_group_id": "source_group_0", - "subcircuit_id": null, + "subcircuit_id": undefined, "type": "schematic_group", "width": 0.4, },