Files
Entropyk/apps/web/src/lib/componentMeta.test.ts
sepehr 3358b74342 Add diagram workbench UI with Modelica DoF coaching and ISO glyphs.
Ship the Next.js cycle editor with CAD chrome, technical HX symbols, Fixed/Free boundary guidance, and secondary water/air pressure drop support in the solver stack.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 22:46:46 +02:00

214 lines
6.2 KiB
TypeScript

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",
]),
);
}
});
});