Prepare Portainer deployment

This commit is contained in:
Zachariah K. Sharma
2026-06-05 23:46:10 -06:00
commit e6f1c5c13f
84 changed files with 12383 additions and 0 deletions
+154
View File
@@ -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);
});
});