test: unit and end-to-end suites
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
27fafa0bcd
commit
9b00e21c44
49 files changed
+6694
-1
No files matched your search
@@ -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);
|
||||
});
|
||||
Reference in new issue
Block a user