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));
	}
});