import { expect, test } from "@playwright/test"; import cases from "../../../tests/fixtures/parameter-expressions.json" with { type: "json" }; import { evaluateParameterExpression } from "../../src/parameterExpression"; import { componentVersionNotices, decodeProjectParameters, encodeProjectParameters } from "../../src/projectCompatibility"; const definitions = [{ type: "chamber", modelType: "chamber", modelVersion: "2", parameters: { p0: { unit: "Pa", quantity: "pressure" }, T0: { unit: "K", quantity: "temperature" }, } }]; function project(value: number | string, version: 1 | 2 = 2) { return { projectSchemaVersion: version, nodes: [{ id: "c", data: { label: "c", componentType: "chamber", modelType: "chamber", modelVersion: "1", parameters: { p0: value, T0: 20 }, parameterUnits: { p0: "bar", T0: "degC" } } }] }; } test("shared expression grammar and resource limits", () => { for (const [source, expected] of cases.valid) { const actual = evaluateParameterExpression(String(source)); expect(actual.ok, String(source)).toBe(true); if (actual.ok) expect(actual.value).toBeCloseTo(Number(expected), 12); } for (const source of [...cases.invalid, "(".repeat(34) + "1" + ")".repeat(34), "1+".repeat(256) + "1", "1".repeat(513), `min(${Array(17).fill("1").join(",")})`]) { expect(evaluateParameterExpression(source).ok, source).toBe(false); } }); test("v2 selected-unit numbers and expression values agree; legacy numbers retain SI", () => { for (const value of [2.5, "2.5", "=2.5", "2+0.5"]) { const input = project(value); const original = structuredClone(input); const internal = decodeProjectParameters(input, definitions); const parameter = internal.nodes[0].data.parameters.p0; const actual = typeof parameter === "number" ? parameter : (() => { const e = evaluateParameterExpression(parameter); return e.ok ? e.value * 1e5 : NaN; })(); expect(actual).toBe(250000); expect(internal.nodes[0].data.parameters.T0).toBe(293.15); const exported = encodeProjectParameters(internal, definitions); expect(exported.projectSchemaVersion).toBe(2); expect(decodeProjectParameters(exported, definitions)).toEqual(internal); expect(input).toEqual(original); } expect(decodeProjectParameters(project(250000, 1), definitions).nodes[0].data.parameters.p0).toBe(250000); expect(() => encodeProjectParameters(project(2.5), definitions)).toThrow(/重复换算/); }); test("version index finds stale and missing versions without mutating or per-node fetch", () => { const input = project(2.5); const original = structuredClone(input); expect(componentVersionNotices(input.nodes, definitions)).toEqual([ { componentId: "c", label: "c", storedVersion: "1", currentVersion: "2" }, ]); expect(input).toEqual(original); input.nodes[0].data.modelVersion = "2"; expect(componentVersionNotices(input.nodes, definitions)).toEqual([]); }); test("export preserves exact SI doubles when display-unit round trip loses a bit", () => { const input = project(15299999.999999998, 1); input.nodes[0].data.parameterUnits.p0 = "bar"; input.nodes[0].data.parameters.T0 = 293.15; const encoded = encodeProjectParameters(input, definitions); expect(encoded.nodes[0].data.parameterUnits.p0).toBe("Pa"); expect(encoded.nodes[0].data.parameters.p0).toBe(15299999.999999998); expect(decodeProjectParameters(encoded, definitions).nodes[0].data.parameters).toEqual(input.nodes[0].data.parameters); });