AnasSarkiz/usb-c-pwm-pa25
A component library defining an AO3400A N-channel MOSFET, 47 µF polarized capacitor, 3-pin through-hole connector, and 9.1 V Zener diode with PCB footprints and 3D CAD models.
- Version
- 0.1.0-prototype.2
- License
- unset
- Stars
- 0
tests/board.test.ts
import { expect, test } from "bun:test";
import { file } from "bun";
import {
any_source_component,
source_net,
source_port,
pcb_via,
pcb_trace,
schematic_sheet,
} from "circuit-json";
import { z } from "zod";
const records = z
.array(z.object({ type: z.string() }).passthrough())
.parse(
await file(new URL("../dist/index/circuit.json", import.meta.url)).json(),
);
const components = records
.filter((r) => r.type === "source_component")
.map((r) => any_source_component.parse(r))
.filter((component) => component.type === "source_component");
const ports = records
.filter((r) => r.type === "source_port")
.map((r) => source_port.parse(r));
const nets = records
.filter((r) => r.type === "source_net")
.map((r) => source_net.parse(r));
const vias = records
.filter((r) => r.type === "pcb_via")
.map((r) => pcb_via.parse(r));
const sheets = records
.filter((r) => r.type === "schematic_sheet")
.map((r) => schematic_sheet.parse(r));
function connectedNet(componentName: string, pin: string) {
const component = components.find((c) => c.name === componentName);
if (!component) throw new Error(`Missing component ${componentName}`);
const port = ports.find(
(p) =>
p.source_component_id === component.source_component_id &&
p.port_hints?.includes(pin),
);
if (!port?.subcircuit_connectivity_map_key)
throw new Error(`Unconnected ${componentName}.${pin}`);
const net = nets.find(
(n) =>
n.subcircuit_connectivity_map_key ===
port.subcircuit_connectivity_map_key,
);
if (!net) throw new Error(`Unnamed connection on ${componentName}.${pin}`);
return net.name;
}
test("power must pass PD negotiation, input protection, regulation, and reverse blocking", () => {
expect(connectedNet("U1", "PPHV1")).toBe("PD_OUT");
expect(connectedNet("U2", "IN")).toBe("PD_OUT");
expect(connectedNet("U2", "OUT")).toBe("V15");
expect(connectedNet("U4", "VIN")).toBe("V15");
expect(connectedNet("L1", "pin2")).toBe("V12_BUCK");
expect(connectedNet("U5", "IN")).toBe("V12_BUCK");
expect(connectedNet("U5", "OUT")).toBe("VM");
expect(connectedNet("U6", "VM")).toBe("VM");
expect(connectedNet("C23", "pin1")).toBe("VM");
});
test("hardware faults override the firmware drive request and reset defaults disable power", () => {
for (const [name, pin] of [
["U2", "FLT"],
["U2", "AUXOFF"],
["U5", "FLT"],
["U5", "AUXOFF"],
["U6", "nFAULT"],
["U8", "OUT_B"],
["U10", "B"],
]) {
expect(connectedNet(name, pin)).toBe("HEALTH");
}
expect(connectedNet("U10", "Y")).toBe(connectedNet("U6", "nSLEEP"));
expect(connectedNet("Q2", "D")).toBe(connectedNet("U2", "EN_UVLO"));
expect(connectedNet("Q3", "D")).toBe(connectedNet("U5", "EN_UVLO"));
expect(connectedNet("R15", "pin1")).toBe("V3V3");
expect(connectedNet("R15", "pin2")).toBe(connectedNet("Q2", "G"));
for (const name of ["R33", "R34", "R55", "R61"])
expect(connectedNet(name, "pin2")).toBe("GND");
});
test("both USB orientations, gate protection, motor polarity and standard JST SWD", () => {
for (const cc of ["CC1", "CC2"])
expect(connectedNet("J1", cc)).toBe(connectedNet("U1", cc));
expect(connectedNet("D2", "IO1")).toBe("CC1");
expect(connectedNet("D2", "IO2")).toBe("CC2");
expect(connectedNet("D6", "cathode")).toBe("CLAMP_GATE");
expect(connectedNet("D6", "anode")).toBe("GND");
expect(connectedNet("J2", "pin1")).toBe(connectedNet("U6", "OUT1"));
expect(connectedNet("J2", "pin2")).toBe(connectedNet("U6", "OUT2"));
for (const [index, net] of [
"V3V3",
"SWDIO",
"GND",
"SWCLK",
"NRST",
].entries())
expect(connectedNet("J5", `pin${index + 1}`)).toBe(net);
});
test("MOSFET and shunt-reference symbols retain the manufacturer's physical pin numbers", () => {
for (const [name, gate, drain] of [
["Q1", "CLAMP_GATE", "BRAKE_DRAIN"],
["Q2", "PWR_OFF", "INPUT_EN"],
["Q3", "PWR_OFF", "MOTOR_UV"],
]) {
expect(connectedNet(name, "pin1")).toBe(gate);
expect(connectedNet(name, "pin2")).toBe("GND");
expect(connectedNet(name, "pin3")).toBe(drain);
}
expect(connectedNet("U7", "pin1")).toBe("VREF_2V5");
expect(connectedNet("U7", "pin2")).toBe("GND");
expect(connectedNet("U7", "pin3")).toBe("GND");
});
test("six native A4 sheets and exact supplier identities for every assembled part", () => {
expect(sheets).toHaveLength(6);
for (const sheet of sheets) {
expect(sheet.sheet_size).toBe("a4");
expect(sheet.sheet_width).toBe(297);
expect(sheet.sheet_height).toBe(210);
}
for (const component of components.filter(
(c) => !/^(TP|H|FID)/.test(c.name),
)) {
expect(component.manufacturer_part_number, component.name).toBeTruthy();
expect(
component.supplier_part_numbers?.jlcpcb?.[0],
component.name,
).toMatch(/^C\d+$/);
}
});
// Validate the actual geometric fields, including native fiducials whose
// pcb_component_id is null. circuit-json 0.0.499's full metadata schema does not
// accept that native output or numeric display offsets; geometry is independent.
const point = z.object({ x: z.number(), y: z.number() });
const rect = point.extend({
width: z.number().positive(),
height: z.number().positive(),
});
const padGeometry = z.discriminatedUnion("shape", [
rect.extend({ shape: z.literal("rect") }),
rect.extend({ shape: z.literal("pill"), radius: z.number().positive() }),
point.extend({ shape: z.literal("circle"), radius: z.number().positive() }),
z.object({ shape: z.literal("polygon"), points: z.array(point).min(3) }),
]);
const pads = records
.filter((r) => r.type === "pcb_smtpad")
.map((r) => ({ id: r.pcb_smtpad_id, geometry: padGeometry.parse(r) }));
type Point = z.infer<typeof point>;
type Pad = z.infer<typeof padGeometry>;
function segmentDistance(point: Point, ends: readonly [Point, Point]) {
const [start, end] = ends;
const dx = end.x - start.x,
dy = end.y - start.y;
const lengthSquared = dx * dx + dy * dy;
const fraction =
lengthSquared === 0
? 0
: Math.max(
0,
Math.min(
1,
((point.x - start.x) * dx + (point.y - start.y) * dy) /
lengthSquared,
),
);
return Math.hypot(
point.x - start.x - fraction * dx,
point.y - start.y - fraction * dy,
);
}
function padDistance(position: Point, pad: Pad) {
if (pad.shape === "circle")
return Math.max(
0,
Math.hypot(position.x - pad.x, position.y - pad.y) - pad.radius,
);
if (pad.shape === "rect")
return Math.hypot(
Math.max(0, Math.abs(position.x - pad.x) - pad.width / 2),
Math.max(0, Math.abs(position.y - pad.y) - pad.height / 2),
);
if (pad.shape === "pill") {
const horizontal = pad.width >= pad.height;
const halfStraight = (Math.max(pad.width, pad.height) - 2 * pad.radius) / 2;
return Math.max(
0,
segmentDistance(position, [
{
x: pad.x - (horizontal ? halfStraight : 0),
y: pad.y - (horizontal ? 0 : halfStraight),
},
{
x: pad.x + (horizontal ? halfStraight : 0),
y: pad.y + (horizontal ? 0 : halfStraight),
},
]) - pad.radius,
);
}
let inside = false,
distance = Infinity;
for (
let index = 0, previous = pad.points.length - 1;
index < pad.points.length;
previous = index++
) {
const a = pad.points[index],
b = pad.points[previous];
if (
a.y > position.y !== b.y > position.y &&
position.x < ((b.x - a.x) * (position.y - a.y)) / (b.y - a.y) + a.x
)
inside = !inside;
distance = Math.min(distance, segmentDistance(position, [a, b]));
}
return inside ? 0 : distance;
}
test("ordinary via holes clear every SMT pad, including same-net pads", () => {
expect(vias.length).toBeGreaterThanOrEqual(4);
for (const via of vias) {
expect(via.hole_diameter).toBeGreaterThanOrEqual(0.3);
expect(via.outer_diameter - via.hole_diameter).toBeGreaterThanOrEqual(
0.29999,
);
expect(via.layers).toEqual(["top", "inner1", "inner2", "bottom"]);
for (const pad of pads)
expect(
padDistance(via, pad.geometry) - via.hole_diameter / 2,
`${via.pcb_via_id} / ${pad.id}`,
).toBeGreaterThanOrEqual(0.19999);
}
});
test("no unresolved native error records", () => {
expect(records.filter((r) => r.type.endsWith("_error"))).toEqual([]);
});
test("generated copper respects the 0.15 mm minimum signal width", () => {
for (const trace of records
.filter((record) => record.type === "pcb_trace")
.map((record) => pcb_trace.parse(record))) {
for (const point of trace.route) {
if (point.route_type !== "wire") continue;
expect(point.width, trace.pcb_trace_id).toBeGreaterThanOrEqual(0.14999);
}
}
});