107 lines
3.6 KiB
JavaScript
107 lines
3.6 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { blankPlan } from "../shared/fields.js";
|
|
import { computeLedger } from "../shared/ledger.js";
|
|
import {
|
|
buildPlanCurve,
|
|
curveSegments,
|
|
curveWarnings,
|
|
} from "../shared/curve.js";
|
|
import { renderReportHtml } from "../shared/report.js";
|
|
import { parseDuration } from "../shared/time.js";
|
|
function plan() {
|
|
const p = blankPlan();
|
|
Object.assign(p.fields, {
|
|
0.1: "Burundi",
|
|
0.4: "500",
|
|
1.4: "8:45",
|
|
4.1: "light",
|
|
4.2: "12",
|
|
4.3: "1:38",
|
|
3.2: "-0:30",
|
|
1.7: "-0:05",
|
|
});
|
|
return p;
|
|
}
|
|
test("light development does not silently collapse to 1:03; explicit override wins", () => {
|
|
const p = plan();
|
|
let l = computeLedger(p);
|
|
assert.equal(l.rawC, 63);
|
|
assert.equal(l.C, 75);
|
|
assert.ok(l.warnings.some((w) => w.includes("below 1:15")));
|
|
p.fields["4.4"] = "1:20";
|
|
assert.equal(computeLedger(p).C, 80);
|
|
});
|
|
test("machine profiles cannot modify plan timing or temperature defaults", () => {
|
|
const p = plan(),
|
|
l = computeLedger(p);
|
|
const machine = {
|
|
pace: { value: 1.8 },
|
|
n: 99,
|
|
medians: { firstCrackTempC: 140, turningPointS: 10 },
|
|
};
|
|
assert.deepEqual(computeLedger(p, machine), l);
|
|
assert.deepEqual(buildPlanCurve(p, l, machine), buildPlanCurve(p, l));
|
|
assert.equal(buildPlanCurve(p, l)[3].tempC, 200);
|
|
});
|
|
test("default development average rise declines and cubic segments do not overshoot", () => {
|
|
const p = plan(),
|
|
l = computeLedger(p),
|
|
points = buildPlanCurve(p, l);
|
|
assert.deepEqual(curveWarnings(points), []);
|
|
for (const s of curveSegments(points).segments) {
|
|
for (let t = 0; t <= 1; t += 0.01) {
|
|
const v =
|
|
(1 - t) ** 3 * s.a.tempC +
|
|
3 * (1 - t) ** 2 * t * s.c1.tempC +
|
|
3 * (1 - t) * t * t * s.c2.tempC +
|
|
t ** 3 * s.b.tempC;
|
|
assert.ok(
|
|
v >= Math.min(s.a.tempC, s.b.tempC) - 1e-8 &&
|
|
v <= Math.max(s.a.tempC, s.b.tempC) + 1e-8,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
test("explicit flick-producing targets are preserved and flagged", () => {
|
|
const p = plan();
|
|
p.temps.drop.tempC = "230";
|
|
const points = buildPlanCurve(p, computeLedger(p));
|
|
assert.equal(points.at(-1).tempC, 230);
|
|
assert.ok(curveWarnings(points).length);
|
|
});
|
|
test("negative timings and implausible density are surfaced", () => {
|
|
const p = plan();
|
|
p.fields["1.6"] = "-10:00";
|
|
p.fields["5.2"] = "76.14";
|
|
const l = computeLedger(p);
|
|
assert.equal(l.A, null);
|
|
assert.ok(l.warnings.some((w) => w.includes("761.4")));
|
|
assert.equal(parseDuration("1:99"), null);
|
|
assert.equal(parseDuration("9".repeat(400)), null);
|
|
});
|
|
test("standalone report has Fahrenheit, grams, graph annotations and escapes untrusted content", () => {
|
|
const p = plan();
|
|
p.fields["0.1"] = "</title><script>alert(1)</script>";
|
|
p.afterRoast.cupNotes = "<img src=x onerror=alert(1)>";
|
|
const html = renderReportHtml(p);
|
|
assert.ok(html.startsWith("<!doctype html>"));
|
|
assert.ok(html.includes("440 g"));
|
|
assert.ok(html.includes("500 g"));
|
|
assert.ok(html.includes("392°F"));
|
|
assert.ok(html.includes("Development · 1:15"));
|
|
assert.ok(html.includes("8:45"));
|
|
assert.ok(html.includes("Maillard"));
|
|
assert.ok(html.includes("@media print"));
|
|
assert.ok(!html.includes("<script>"));
|
|
assert.ok(!html.includes("<img"));
|
|
assert.ok(!html.includes("°C"));
|
|
assert.ok(!html.includes("Hottop"));
|
|
assert.ok(!html.includes("<script src"));
|
|
});
|
|
test("incomplete plans export without NaN, Infinity or invented output weight", () => {
|
|
const html = renderReportHtml(blankPlan());
|
|
assert.ok(!/NaN|Infinity/.test(html));
|
|
assert.ok(!html.includes("0 g"));
|
|
});
|