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>
214 lines
6.2 KiB
TypeScript
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",
|
|
]),
|
|
);
|
|
}
|
|
});
|
|
});
|