test: unit and end-to-end suites

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Your NameandClaude Sonnet 5.5 committed 2026-10-15 11:57:09 +02:00
1 parent 27fafa0bcd
commit 9b00e21c44
49 files changed
+6694 -1

No files matched your search

+89
View File
@@ -0,0 +1,89 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { evaluateFormula, parseFormula, FormulaError, show } from '../src/lib/formula.js';
const vars = { Price: 12.5, Qty: 4, Name: 'Cleanser', Brand: 'Balea', Opened: '2026-03-01', Expiry: '2026-09-01', Done: true, Tags: ['a', 'b'], Empty: null };
const ctx = { today: '2026-03-31', prop: (n) => { if (!(n in vars)) throw new FormulaError(`Unknown property "${n}"`); return vars[n]; } };
const ev = (s) => evaluateFormula(s, ctx);
test('arithmetic, precedence and properties', () => {
assert.equal(ev('1 + 2 * 3'), 7);
assert.equal(ev('(1 + 2) * 3'), 9);
assert.equal(ev('2 ^ 3 ^ 2'), 512); // right-associative
assert.equal(ev('-2 ^ 2'), -4); // as in maths: the power first
assert.equal(ev('prop("Price") * prop("Qty")'), 50);
assert.equal(ev('{Price} * {Qty}'), 50);
assert.equal(ev('10 % 4'), 2);
assert.equal(ev('1 / 4'), 0.25);
assert.equal(ev('prop("Empty") + 5'), 5); // empty counts as 0
assert.equal(show(ev('0.1 + 0.2')), '0.3');
});
test('comparisons, logic and if', () => {
assert.equal(ev('3 > 2 and 2 >= 2'), true);
assert.equal(ev('1 > 2 or not false'), true);
assert.equal(ev('!(1 == 1)'), false);
assert.equal(ev('prop("Name") == "Cleanser"'), true);
assert.equal(ev('prop("Empty") == ""'), true);
assert.equal(ev('if(prop("Price") > 10, "pricey", "cheap")'), 'pricey');
assert.equal(ev('if(false, 1)'), null);
assert.equal(ev('if(prop("Empty"), "x", "y")'), 'y');
assert.equal(ev('and(true, 1, "x")'), true);
assert.equal(ev('if(true, 1, 1 / 0)'), 1); // the other branch is not evaluated
});
test('text and lists', () => {
assert.equal(ev('concat(prop("Brand"), " – ", prop("Name"))'), 'Balea – Cleanser');
assert.equal(ev('"a" + "b"'), 'ab');
assert.equal(ev('"Total: " + 5'), 'Total: 5');
assert.equal(ev('upper(prop("Name"))'), 'CLEANSER');
assert.equal(ev('length(prop("Tags"))'), 2);
assert.equal(ev('join(prop("Tags"), "/")'), 'a/b');
assert.equal(ev('contains(prop("Tags"), "B")'), true);
assert.equal(ev('contains("Hello", "ell")'), true);
assert.equal(ev('replace("a-b-c", "-", "+")'), 'a+b+c');
});
test('numbers functions', () => {
assert.equal(ev('round(2.567, 1)'), 2.6);
assert.equal(ev('round(2.5)'), 3);
assert.equal(ev('floor(2.9) + ceil(2.1)'), 5);
assert.equal(ev('abs(-3)'), 3);
assert.equal(ev('min(3, 1, 2)'), 1);
assert.equal(ev('max(prop("Tags"), 5)'.replace('prop("Tags"), ', '')), 5);
assert.equal(ev('sum(1, 2, 3)'), 6);
assert.equal(ev('average(2, 4)'), 3);
assert.equal(ev('toNumber("€12.50")'), 12.5);
});
test('dates', () => {
assert.equal(ev('today()'), '2026-03-31');
assert.equal(ev('dateBetween(prop("Expiry"), today(), "days")'), 154);
assert.equal(ev('dateBetween(prop("Expiry"), prop("Opened"), "months")'), 6);
assert.equal(ev('dateBetween(today(), prop("Expiry"), "days")'), -154);
assert.equal(ev('dateAdd(prop("Opened"), 12, "months")'), '2027-03-01');
assert.equal(ev('dateAdd("2026-01-31", 1, "months")'), '2026-02-28');
assert.equal(ev('dateAdd(prop("Opened"), 2, "weeks")'), '2026-03-15');
assert.equal(ev('prop("Expiry") - prop("Opened")'), 184); // date - date = days
assert.equal(ev('prop("Opened") + 30'), '2026-03-31'); // date + number = days later
assert.equal(ev('prop("Expiry") > today()'), true);
assert.equal(ev('year(prop("Opened")) * 100 + month(prop("Opened"))'), 202603);
assert.equal(ev('formatDate(prop("Opened"), "MMM D, YYYY")'), 'Mar 1, 2026');
});
test('mistakes are reported, not thrown as anything else', () => {
const msg = (s) => { try { ev(s); return null; } catch (e) { assert.ok(e instanceof FormulaError, s + ' -> ' + e); return e.message; } };
assert.match(msg('1 +'), /ends too soon/);
assert.match(msg('1 / 0'), /Division by zero/);
assert.match(msg('prop("Nope")'), /Unknown property/);
assert.match(msg('nope(1)'), /no function/);
assert.match(msg('Price * 2'), /not a function or a value/);
assert.match(msg('"abc" * 2'), /can't be used as a number/);
assert.match(msg('(1 + 2'), /Expected “\)”/);
assert.match(msg('1 $ 2'), /Unexpected/);
assert.match(msg('"open'), /not closed/);
assert.match(msg(''), /empty/);
assert.match(msg('dateBetween("x", today(), "days")'), /can't be used as a date/);
assert.match(msg('dateBetween(today(), today(), "decades")'), /Unknown unit/);
assert.throws(() => parseFormula('1 +'), FormulaError);
});