import { describe, it, expect } from "vitest"; import { COMPONENTS, COMPONENT_BY_TYPE, COMPONENT_CATEGORIES, defaultParams, isSecondaryPort, nodeSize, portLabel, portRole, portSide, } from "./componentMeta"; describe("portSide", () => { it("places inlet/suction on the left", () => { expect(portSide("inlet")).toBe("left"); expect(portSide("inlet_a")).toBe("left"); expect(portSide("suction")).toBe("left"); }); it("places outlet/discharge on the right", () => { expect(portSide("outlet")).toBe("right"); expect(portSide("outlet_b")).toBe("right"); expect(portSide("discharge")).toBe("right"); }); it("places liquid/vapor outlets on the bottom and economizer on top", () => { expect(portSide("liquid_outlet")).toBe("bottom"); expect(portSide("vapor_outlet")).toBe("bottom"); expect(portSide("economizer")).toBe("top"); }); it("places the caloporteur inlet at the bottom and outlet at the top", () => { expect(portSide("secondary_inlet")).toBe("bottom"); expect(portSide("secondary_outlet")).toBe("top"); }); it("defaults unknown ports to the left", () => { expect(portSide("mystery")).toBe("left"); }); }); describe("secondary (caloporteur) ports", () => { it("identifies the secondary ports", () => { expect(isSecondaryPort("secondary_inlet")).toBe(true); expect(isSecondaryPort("secondary_outlet")).toBe(true); expect(isSecondaryPort("inlet")).toBe(false); expect(isSecondaryPort("outlet")).toBe(false); }); it("treats the secondary inlet as a target and outlet as a source", () => { expect(portRole("secondary_inlet")).toBe("target"); expect(portRole("secondary_outlet")).toBe("source"); }); it("labels the secondary ports as htf", () => { expect(portLabel("secondary_inlet")).toBe("htf·in"); expect(portLabel("secondary_outlet")).toBe("htf·out"); }); it("exposes caloporteur ports on the main heat exchangers", () => { for (const type of [ "Condenser", "Evaporator", "FloodedEvaporator", "FinCoilCondenser", "BphxEvaporator", "BphxCondenser", ]) { const meta = COMPONENT_BY_TYPE[type]; expect(meta.ports, type).toContain("secondary_inlet"); expect(meta.ports, type).toContain("secondary_outlet"); } }); }); describe("portRole", () => { it("treats outlets/discharge/*_outlet as sources", () => { expect(portRole("outlet")).toBe("source"); expect(portRole("discharge")).toBe("source"); expect(portRole("liquid_outlet")).toBe("source"); expect(portRole("hot_outlet")).toBe("source"); }); it("treats inlets/suction as targets", () => { expect(portRole("inlet")).toBe("target"); expect(portRole("suction")).toBe("target"); expect(portRole("cold_inlet")).toBe("target"); }); }); describe("nodeSize", () => { it("gives compressors a square footprint", () => { const s = nodeSize("IsentropicCompressor"); expect(s.w).toBe(s.h); }); it("makes heat exchangers wider than tall", () => { const s = nodeSize("Condenser"); expect(s.w).toBeGreaterThan(s.h); }); it("makes the separator (drum) taller than wide", () => { const s = nodeSize("Drum"); expect(s.h).toBeGreaterThan(s.w); }); it("returns positive dimensions for unknown types", () => { const s = nodeSize("Whatever"); expect(s.w).toBeGreaterThan(0); expect(s.h).toBeGreaterThan(0); }); }); describe("portLabel", () => { it("abbreviates known ports", () => { expect(portLabel("inlet")).toBe("in"); expect(portLabel("discharge")).toBe("dis"); expect(portLabel("economizer")).toBe("eco"); }); it("falls back to the raw port name", () => { expect(portLabel("custom_port")).toBe("custom_port"); }); }); describe("defaultParams", () => { it("returns the declared default for each param", () => { const params = defaultParams("IsentropicCompressor"); expect(params.isentropic_efficiency).toBe(0.75); expect(params.t_cond_k).toBe(323.15); }); it("returns an empty object for an unknown type", () => { expect(defaultParams("DoesNotExist")).toEqual({}); }); }); describe("COMPONENTS catalogue integrity", () => { it("has a unique type for every component", () => { const types = COMPONENTS.map((c) => c.type); expect(new Set(types).size).toBe(types.length); }); it("indexes every component in COMPONENT_BY_TYPE", () => { for (const c of COMPONENTS) { expect(COMPONENT_BY_TYPE[c.type]).toBe(c); } }); it("declares at least one port per component", () => { for (const c of COMPONENTS) { if (c.category === "Controls" || c.category === "Advanced") continue; if (c.type === "SaturatedController") continue; expect(c.ports.length).toBeGreaterThan(0); } }); it("uses only declared categories", () => { for (const c of COMPONENTS) { expect(COMPONENT_CATEGORIES).toContain(c.category); } }); it("has unique param keys within each component", () => { for (const c of COMPONENTS) { const keys = c.params.map((p) => p.key); expect(new Set(keys).size, `duplicate param in ${c.type}`).toBe(keys.length); } }); it("uses hex colours", () => { for (const c of COMPONENTS) { expect(c.color, c.type).toMatch(/^#[0-9a-fA-F]{6}$/); } }); it("exposes detailed fin-coil engineering geometry", () => { const keys = COMPONENT_BY_TYPE.FinCoilCondenser.params.map((param) => param.key); expect(keys).toEqual( expect.arrayContaining([ "manufacturer", "model", "face_width_m", "face_height_m", "n_rows", "fin_type", "fin_pitch_fpi", "tube_od_mm", "tube_pitch_mm", "air_face_velocity_m_s", ]), ); }); it("exposes manufacturer and plate-pack data for both BPHX models", () => { for (const type of ["BphxEvaporator", "BphxCondenser"]) { const keys = COMPONENT_BY_TYPE[type].params.map((param) => param.key); expect(keys).toEqual( expect.arrayContaining([ "manufacturer", "model", "n_plates", "plate_length_m", "plate_width_m", "channel_spacing_mm", "chevron_angle_deg", "correlation", ]), ); } }); });