Prepare Portainer deployment
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { evalExpr, applyCalculations } from "./calc";
|
||||
import type { Field } from "./types";
|
||||
|
||||
describe("evalExpr — arithmetic", () => {
|
||||
it("evaluates simple arithmetic", () => {
|
||||
expect(evalExpr("1 + 2", {})).toBe(3);
|
||||
expect(evalExpr("10 - 4", {})).toBe(6);
|
||||
expect(evalExpr("3 * 4", {})).toBe(12);
|
||||
expect(evalExpr("12 / 4", {})).toBe(3);
|
||||
expect(evalExpr("10 % 3", {})).toBe(1);
|
||||
});
|
||||
|
||||
it("honors operator precedence", () => {
|
||||
expect(evalExpr("2 + 3 * 4", {})).toBe(14);
|
||||
expect(evalExpr("(2 + 3) * 4", {})).toBe(20);
|
||||
expect(evalExpr("10 - 2 - 3", {})).toBe(5); // left-associative
|
||||
});
|
||||
|
||||
it("handles unary minus", () => {
|
||||
expect(evalExpr("-5 + 10", {})).toBe(5);
|
||||
expect(evalExpr("-(2 + 3)", {})).toBe(-5);
|
||||
});
|
||||
|
||||
it("returns 0 on divide by zero (graceful, not NaN)", () => {
|
||||
expect(evalExpr("5 / 0", {})).toBe(0);
|
||||
});
|
||||
|
||||
it("concatenates strings with +", () => {
|
||||
expect(evalExpr("'hello ' + 'world'", {})).toBe("hello world");
|
||||
expect(evalExpr("'count: ' + 5", {})).toBe("count: 5");
|
||||
});
|
||||
});
|
||||
|
||||
describe("evalExpr — variables", () => {
|
||||
it("looks up bare identifiers from the values map", () => {
|
||||
expect(evalExpr("a + b", { a: 3, b: 4 })).toBe(7);
|
||||
});
|
||||
|
||||
it("returns null for missing variables", () => {
|
||||
expect(evalExpr("nonexistent", {})).toBeNull();
|
||||
});
|
||||
|
||||
it("recognizes true/false/null constants", () => {
|
||||
expect(evalExpr("true", {})).toBe(true);
|
||||
expect(evalExpr("false", {})).toBe(false);
|
||||
expect(evalExpr("null", {})).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("evalExpr — comparisons & logic", () => {
|
||||
it("compares numbers", () => {
|
||||
expect(evalExpr("5 > 3", {})).toBe(true);
|
||||
expect(evalExpr("3 >= 3", {})).toBe(true);
|
||||
expect(evalExpr("2 < 1", {})).toBe(false);
|
||||
expect(evalExpr("4 == 4", {})).toBe(true);
|
||||
expect(evalExpr("4 != 5", {})).toBe(true);
|
||||
});
|
||||
|
||||
it("short-circuits && and ||", () => {
|
||||
expect(evalExpr("true && false", {})).toBe(false);
|
||||
expect(evalExpr("true || false", {})).toBe(true);
|
||||
expect(evalExpr("1 && 2", {})).toBe(true); // truthy && truthy → true
|
||||
});
|
||||
|
||||
it("handles ! unary", () => {
|
||||
expect(evalExpr("!true", {})).toBe(false);
|
||||
expect(evalExpr("!false", {})).toBe(true);
|
||||
expect(evalExpr("!0", {})).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("evalExpr — functions", () => {
|
||||
it("if(condition, then, else)", () => {
|
||||
expect(evalExpr("if(true, 1, 2)", {})).toBe(1);
|
||||
expect(evalExpr("if(false, 1, 2)", {})).toBe(2);
|
||||
expect(evalExpr("if(0, 'a', 'b')", {})).toBe("b");
|
||||
});
|
||||
|
||||
it("sum, avg, min, max flatten and tolerate non-numbers", () => {
|
||||
expect(evalExpr("sum(1, 2, 3)", {})).toBe(6);
|
||||
expect(evalExpr("avg(2, 4, 6)", {})).toBe(4);
|
||||
expect(evalExpr("min(5, 2, 8)", {})).toBe(2);
|
||||
expect(evalExpr("max(5, 2, 8)", {})).toBe(8);
|
||||
});
|
||||
|
||||
it("avg on empty input returns 0, not NaN", () => {
|
||||
expect(evalExpr("avg()", {})).toBe(0);
|
||||
});
|
||||
|
||||
it("count skips empty values", () => {
|
||||
expect(evalExpr("count(1, '', 2, null, 3)", {})).toBe(3);
|
||||
});
|
||||
|
||||
it("len on strings and arrays", () => {
|
||||
expect(evalExpr("len('hello')", {})).toBe(5);
|
||||
expect(evalExpr("len(items)", { items: [1, 2, 3] })).toBe(3);
|
||||
});
|
||||
|
||||
it("num coerces to a number", () => {
|
||||
expect(evalExpr("num('42')", {})).toBe(42);
|
||||
expect(evalExpr("num(null)", {})).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("evalExpr — safety", () => {
|
||||
it("returns null on syntax errors (doesn't throw)", () => {
|
||||
expect(evalExpr("1 +", {})).toBeNull();
|
||||
expect(evalExpr("(((", {})).toBeNull();
|
||||
expect(evalExpr("@@@", {})).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects unknown functions safely", () => {
|
||||
expect(evalExpr("nope(1, 2)", {})).toBeNull();
|
||||
});
|
||||
|
||||
it("does not let identifiers be eval'd as JS (no prototype, no global)", () => {
|
||||
expect(evalExpr("constructor", {})).toBeNull();
|
||||
expect(evalExpr("__proto__", {})).toBeNull();
|
||||
expect(evalExpr("globalThis", {})).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyCalculations", () => {
|
||||
const fields: Field[] = [
|
||||
{ id: "a", type: "number", label: "A" },
|
||||
{ id: "b", type: "number", label: "B" },
|
||||
{ id: "sum_ab", type: "calculated", label: "Sum", expression: "a + b" },
|
||||
{ id: "doubled", type: "calculated", label: "Doubled", expression: "sum_ab * 2" },
|
||||
];
|
||||
|
||||
it("computes and injects calculated values", () => {
|
||||
const out = applyCalculations(fields, { a: 2, b: 3 });
|
||||
expect(out.sum_ab).toBe(5);
|
||||
expect(out.doubled).toBe(10);
|
||||
});
|
||||
|
||||
it("computes downstream calculations after upstream ones (in field order)", () => {
|
||||
const out = applyCalculations(fields, { a: 10, b: 1 });
|
||||
expect(out.sum_ab).toBe(11);
|
||||
expect(out.doubled).toBe(22);
|
||||
});
|
||||
|
||||
it("returns null for calculated fields without an expression", () => {
|
||||
const f: Field[] = [{ id: "x", type: "calculated", label: "x" }];
|
||||
expect(applyCalculations(f, {}).x).toBeNull();
|
||||
});
|
||||
|
||||
it("leaves non-calculated fields untouched", () => {
|
||||
const out = applyCalculations(fields, { a: 1, b: 2, extra: "kept" });
|
||||
expect(out.extra).toBe("kept");
|
||||
expect(out.a).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user