// Long-press two rows and the app subtracts their times. The press itself // needs a finger, but everything it decides — which picks are held, what the // bar says — is ordinary logic, and that is where this can go quietly wrong. import { load } from "./extract.mjs"; import { suite, eq, ok, report } from "./assert.mjs"; let rendered = 0; const app = load({ names: [ "EVENT_LABELS", "ymd", "formatDuration", "formatTime", "measurePick", "toggleMeasurePick", "clearMeasure", "measureSummary", "measureLabel", ], lets: ["measurePick"], stubs: { render: () => { rendered++; } }, }); const at = (day, hour, min = 0) => new Date(2026, 8, day, hour, min).getTime(); const ate = { id: "a", type: "eat", at: at(20, 12, 10) }; const poo = { id: "b", type: "poo", at: at(20, 15, 52) }; const pee = { id: "c", type: "pee", at: at(20, 18, 30) }; const lateEat = { id: "d", type: "eat", at: at(19, 18, 30) }; // the evening before const events = [ate, poo, pee, lateEat]; const pick = (...ids) => { app.set.measurePick([]); ids.forEach(app.toggleMeasurePick); }; const held = () => app.get.measurePick(); suite("what a press does to the pick"); { pick("a"); eq(held(), ["a"], "one press holds one"); pick("a", "b"); eq(held(), ["a", "b"], "a second press holds the pair"); // The user's choice: a third is refused rather than rolling the pair on. pick("a", "b", "c"); eq(held(), ["a", "b"], "a third press is ignored while two are held"); // Not a third selection but an undo of one — a mis-press costs one press // rather than starting over. pick("a", "b"); app.toggleMeasurePick("a"); eq(held(), ["b"], "pressing a picked row unpicks it"); app.toggleMeasurePick("c"); eq(held(), ["b", "c"], "…leaving room for a different second"); pick("a", "b"); app.clearMeasure(); eq(held(), [], "clearing drops both"); } suite("the reading"); { const two = app.measureSummary(["a", "b"], events); ok(two.show && two.complete, "two picks give a complete reading"); eq(two.duration, "3h 42m", "12:10 to 15:52 is 3h 42m"); // Pressed newest-first, which is the natural way to scan a log upward. const reversed = app.measureSummary(["b", "a"], events); eq(reversed.duration, "3h 42m", "the order they were pressed in doesn't change the gap"); eq(reversed.text, two.text, "…and it still reads chronologically, earliest first"); ok(/Ate/.test(two.text) && /Poo/.test(two.text), "both events are named"); } suite("a pair that straddles midnight"); { const overnight = app.measureSummary(["d", "b"], events); // 19th 18:30 → 20th 15:52 eq(overnight.duration, "21h 22m", "the gap crosses the day boundary correctly"); ok(/Sep/.test(overnight.text), "the dates are named, since two bare times would be ambiguous across days"); ok(!/Sep/.test(app.measureSummary(["a", "b"], events).text), "…but a same-day pair stays uncluttered"); } suite("an incomplete or stale pick"); { const one = app.measureSummary(["a"], events); ok(one.show && !one.complete, "one pick shows the bar without a duration"); ok(/long-press another/.test(one.text), "…and asks for the second"); eq(app.measureSummary([], events).show, false, "nothing picked hides the bar"); // Deleted here, or tombstoned by another device mid-measurement. const stale = app.measureSummary(["a", "gone"], events); eq(stale.ids, ["a"], "an id that no longer resolves is dropped from the pick"); ok(!stale.complete, "…so what is left is one pick, not a broken pair"); eq(app.measureSummary(["gone", "also-gone"], events).show, false, "both gone hides the bar rather than showing an empty one"); } suite("the label"); { eq(app.measureLabel(ate), `Ate ${app.formatTime(ate.at)}`, "type and time"); ok(/Sep 20/.test(app.measureLabel(ate, true)), "with the date when asked for"); } export default report("measure");