AnasSarkiz/magnetic-shutter-remote-r8
A USB‑C rechargeable battery-powered ESP32‑C3 wireless controller with Li‑ion charging, protected 3.3‑V buck‑boost regulation, thermal/safety monitoring, status LEDs, buttons, programming UART, crystal clock, and 2.4‑GHz antenna matching network.
- Version
- 0.4.0-prototype
- License
- unset
- Stars
- 0
tests/r8-product.test.ts
import { expect, test } from "bun:test";
import { any_circuit_element } from "circuit-json";
import { z } from "zod";
import { dimensions, pcbPointInProduct } from "../product/dimensions";
import { createProductParts } from "../product/geometry";
import { localModuleSpecifiers } from "../scripts/lib/local-module-specifiers";
test("the shared package preview contains electrical PCB, schematic and BOM records", async () => {
const config = z
.object({
previewComponentPath: z.string(),
includeBoardFiles: z.array(z.string()),
})
.parse(await Bun.file("tscircuit.config.json").json());
expect(config.includeBoardFiles).toContain(config.previewComponentPath);
expect(config.includeBoardFiles).toContain("enclosure.circuit.tsx");
const entryName = config.previewComponentPath.replace(/\.circuit\.tsx$/, "");
const circuit = any_circuit_element
.array()
.parse(await Bun.file(`dist/${entryName}/circuit.json`).json());
expect(
circuit.filter((element) => element.type === "pcb_board"),
).toHaveLength(1);
const fittedIds = new Set(
circuit.flatMap((element) =>
element.type === "source_component" ? [element.source_component_id] : [],
),
);
expect(
circuit.filter(
(element) =>
element.type === "pcb_component" &&
fittedIds.has(element.source_component_id),
),
).toHaveLength(63);
const viaHelpers = circuit.filter(
(element) =>
element.type === "pcb_component" &&
!fittedIds.has(element.source_component_id),
);
expect(viaHelpers).toHaveLength(4);
for (const helper of viaHelpers) {
if (helper.type !== "pcb_component")
throw new Error("Unexpected via helper");
expect(helper.source_component_id).toMatch(
/^source_manually_placed_via_[0-3]$/,
);
expect(
circuit.filter(
(element) =>
element.type === "pcb_via" &&
element.x === helper.center.x &&
element.y === helper.center.y &&
element.hole_diameter === 0.3 &&
element.outer_diameter === 0.45 &&
element.layers.includes("top") &&
element.layers.includes("bottom"),
),
).toHaveLength(1);
}
expect(
circuit.filter((element) => element.type === "pcb_smtpad").length,
).toBeGreaterThan(0);
expect(
circuit.filter((element) => element.type === "pcb_trace").length,
).toBeGreaterThan(0);
expect(
circuit.filter((element) => element.type === "schematic_component"),
).toHaveLength(63);
expect(
circuit.filter((element) => element.type === "source_component"),
).toHaveLength(63);
expect(
circuit.filter((element) => element.type.endsWith("_error")),
).toHaveLength(0);
});
test("native product views preserve the board transform and create no second PCB", async () => {
for (const name of ["product.assembly", "product.exploded", "enclosure"]) {
const circuit = any_circuit_element
.array()
.parse(await Bun.file(`dist/${name}/circuit.json`).json());
expect(
circuit.filter((element) => element.type === "pcb_board"),
).toHaveLength(0);
expect(
circuit.filter((element) => element.type === "pcb_trace"),
).toHaveLength(0);
expect(
circuit.filter((element) => element.type.includes("error")),
).toHaveLength(0);
const board = circuit.find(
(element) =>
element.type === "source_component" &&
element.name === "R9PCBAndFittedParts1",
);
if (board?.type !== "source_component")
throw new Error("Missing actual PCB assembly");
const cad = circuit.find(
(element) =>
element.type === "cad_component" &&
element.source_component_id === board.source_component_id,
);
if (cad?.type !== "cad_component")
throw new Error("Missing real PCB model");
expect(cad.position).toEqual({
x: dimensions.remote.centerXMm,
y: dimensions.remote.centerYMm,
z: dimensions.pcb.centerZMm + (name === "product.exploded" ? 26 : 0),
});
expect(cad.rotation).toEqual({ x: 0, y: 0, z: 0 });
expect(cad.model_glb_url).toContain("r8-pcb.glb");
expect(cad.model_unit_to_mm_scale_factor).toBe(1);
expect(cad.model_board_normal_direction).toBe("z+");
}
});
test("native assembly uses all ten lossless mechanical models with compact build JSON", async () => {
const receipt = z
.object({
passes: z.boolean(),
models: z.array(
z.object({
part: z.string(),
file: z.string(),
sha256: z.string(),
triangles: z.number(),
exported_triangles: z.number(),
}),
),
})
.parse(await Bun.file("product/mechanical-model-review.json").json());
expect(receipt.passes).toBe(true);
expect(receipt.models).toHaveLength(10);
for (const view of ["product.assembly", "product.exploded", "enclosure"]) {
const file = Bun.file(`dist/${view}/circuit.json`);
expect(file.size).toBeLessThan(100_000);
const circuit = any_circuit_element.array().parse(await file.json());
for (const model of receipt.models) {
const component = circuit.find(
(element) =>
element.type === "source_component" && element.name === model.part,
);
if (component?.type !== "source_component")
throw new Error("Mechanical part is absent from native assembly");
const cad = circuit.find(
(element) =>
element.type === "cad_component" &&
element.source_component_id === component.source_component_id,
);
if (cad?.type !== "cad_component")
throw new Error("Mechanical part CAD is absent");
expect(cad.model_glb_url).toContain(model.file);
expect(cad.model_unit_to_mm_scale_factor).toBe(1);
expect(cad.model_board_normal_direction).toBe("z+");
expect(model.exported_triangles).toBe(model.triangles);
const hash = new Bun.CryptoHasher("sha256")
.update(await Bun.file(model.file).arrayBuffer())
.digest("hex");
expect(hash).toBe(model.sha256);
}
}
});
test("imported enclosure models retain their source black and bronze materials", async () => {
const materialSchema = z.object({
materials: z.array(
z.object({
pbrMetallicRoughness: z.object({
baseColorFactor: z.tuple([
z.number(),
z.number(),
z.number(),
z.number(),
]),
}),
}),
),
});
for (const part of createProductParts()) {
if (part.plan.type !== "colorize")
throw new Error(`Missing source color: ${part.name}`);
const bytes = await Bun.file(
`product/models/mechanical/${part.name}.glb`,
).arrayBuffer();
const header = new DataView(bytes);
expect(header.getUint32(0, true)).toBe(0x46546c67);
const document = materialSchema.parse(
JSON.parse(
new TextDecoder().decode(
new Uint8Array(bytes, 20, header.getUint32(12, true)),
),
),
);
expect(document.materials).toHaveLength(1);
const rgba = document.materials[0].pbrMetallicRoughness.baseColorFactor;
for (let axis = 0; axis < 3; axis++)
expect(Math.abs(rgba[axis] - part.plan.color[axis])).toBeLessThanOrEqual(
1 / 255,
);
expect(
Math.abs(rgba[3] - (part.referenceOnly ? 0.45 : 1)),
).toBeLessThanOrEqual(1 / 255);
}
});
test("standalone viewer embeds one valid module without external script tags", async () => {
const scripts: { type: string | null; src: string | null }[] = [];
const rewriter = new HTMLRewriter().on("script", {
element(element) {
scripts.push({
type: element.getAttribute("type"),
src: element.getAttribute("src"),
});
},
});
await rewriter
.transform(new Response(Bun.file("dist/enclosure/viewer.html")))
.arrayBuffer();
expect(scripts).toEqual([{ type: "module", src: null }]);
});
test("registry source discovery distinguishes real imports from generated-code strings", () => {
expect(
localModuleSpecifiers({
path: "generator.ts",
source:
'import actual from "./actual"; export { actual } from "./shared"; const generated = `import inputs from "./inputs.json";`; // import fake from "./fake"\n',
}),
).toEqual(["./actual", "./shared"]);
});
test("all real electronic envelopes fit, magnetic datum clearance and solid separation pass", async () => {
const review = z
.object({
clashes: z.array(z.unknown()),
electronicEnvelopeClashes: z.array(z.unknown()),
phoneClearanceViolations: z.array(z.unknown()),
fittedComponentEnvelopesChecked: z.number(),
boardEnvelopeChecked: z.boolean(),
containmentReview: z.object({
boardOutsideVolumeMm3: z.number(),
batteryOutsideVolumeMm3: z.number(),
}),
mountingReview: z.array(
z.object({ lowerSeatAndUpperPillarVerified: z.boolean() }),
),
shoulderWallVolumeMm3: z.number(),
shoulderClosureReview: z.array(
z.object({
centerMm: z.tuple([z.number(), z.number(), z.number()]),
expectedVolumeMm3: z.number(),
missingVolumeMm3: z.number(),
}),
),
shutterTravelReview: z.array(
z.object({
travelMm: z.number(),
caseAndOther43ComponentEnvelopesClear: z.boolean(),
intendedSw2ActuatorContactOnly: z.boolean(),
}),
),
fullProductBoundingBoxMm: z.array(z.array(z.number())),
shutterAlignment: z.object({
freeGapMm: z.number(),
centerYMm: z.number(),
centerZMm: z.number(),
}),
parts: z.array(
z.object({ name: z.string(), boundsMm: z.array(z.array(z.number())) }),
),
})
.parse(await Bun.file("product/geometry-review.json").json());
expect(review.clashes).toEqual([]);
expect(review.electronicEnvelopeClashes).toEqual([]);
expect(review.phoneClearanceViolations).toEqual([]);
expect(review.fittedComponentEnvelopesChecked).toBe(63);
expect(review.boardEnvelopeChecked).toBe(true);
expect(review.containmentReview.boardOutsideVolumeMm3).toBe(0);
expect(review.containmentReview.batteryOutsideVolumeMm3).toBe(0);
expect(review.mountingReview).toHaveLength(4);
expect(
review.mountingReview.every(
(mount) => mount.lowerSeatAndUpperPillarVerified,
),
).toBe(true);
expect(review.shoulderWallVolumeMm3).toBeCloseTo(0.008, 5);
expect(review.shoulderClosureReview.length).toBeGreaterThanOrEqual(21);
for (const sample of review.shoulderClosureReview) {
expect(sample.expectedVolumeMm3).toBeGreaterThan(1e-8);
expect(sample.missingVolumeMm3).toBeLessThanOrEqual(1e-8);
}
expect(review.shutterTravelReview.map((step) => step.travelMm)).toEqual([
0, 0.15, 0.25, 0.35,
]);
expect(
review.shutterTravelReview.every(
(step) =>
step.caseAndOther43ComponentEnvelopesClear &&
step.intendedSw2ActuatorContactOnly,
),
).toBe(true);
review.fullProductBoundingBoxMm[1].forEach((maximum, axis) =>
expect(maximum - review.fullProductBoundingBoxMm[0][axis]).toBeCloseTo(
[90, 78, 34][axis],
5,
),
);
const circularFace = review.parts.find(
(part) => part.name === "MagSafeRearCover",
);
if (!circularFace) throw new Error("Missing reference circular back");
expect(circularFace.boundsMm[1][2]).toBeCloseTo(17.05, 4);
expect(review.shutterAlignment.freeGapMm).toBeCloseTo(0.15, 3);
expect(review.shutterAlignment.centerYMm).toBeCloseTo(
pcbPointInProduct({
x: dimensions.shutter.actuatorXMm,
y: dimensions.shutter.centerYMm,
}).y,
);
expect(review.shutterAlignment.centerZMm).toBe(dimensions.shutter.centerZMm);
const cell = review.parts.find(
(part) => part.name === "ASR00012_MaximumEnvelope",
);
if (!cell) throw new Error("Missing retained protected pack");
expect(
cell.boundsMm[1].map(
(coordinate, index) => coordinate - cell.boundsMm[0][index],
),
).toEqual([32, 43, 8.5]);
const pcb = z
.object({
fittedModels: z.number(),
emptyModels: z.number(),
allFittedCadAnchorsMatch: z.boolean(),
})
.parse(await Bun.file("product/pcb-model-review.json").json());
expect(pcb.fittedModels).toBe(63);
expect(pcb.emptyModels).toBe(0);
expect(pcb.allFittedCadAnchorsMatch).toBe(true);
});
test("eight manufacturing STL files match the checked plans and pass actual readback", async () => {
const receipt = z
.object({
source_plans_sha256: z.string(),
native_geometry_sha256: z.string(),
printed_parts: z.array(
z.object({
file: z.string(),
sha256: z.string(),
connected_solids: z.number(),
zero_area_triangles: z.number(),
watertight: z.boolean(),
winding_consistent: z.boolean(),
native_volume_delta_mm3: z.number(),
}),
),
})
.parse(await Bun.file("product/print-mesh-review.json").json());
const hash = async (path: string) =>
new Bun.CryptoHasher("sha256")
.update(await Bun.file(path).arrayBuffer())
.digest("hex");
expect(receipt.source_plans_sha256).toBe(
await hash("product/print-plans.json"),
);
expect(receipt.native_geometry_sha256).toBe(
await hash("product/geometry-review.json"),
);
expect(receipt.printed_parts).toHaveLength(8);
for (const part of receipt.printed_parts) {
expect(part.connected_solids).toBe(1);
expect(part.zero_area_triangles).toBe(0);
expect(part.watertight).toBe(true);
expect(part.winding_consistent).toBe(true);
expect(part.native_volume_delta_mm3).toBeLessThan(0.00001);
expect(part.sha256).toBe(await hash(part.file));
}
});