mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
feat(medications): add a multi-step forward-jump lookahead to the wizard payload
This commit is contained in:
1 parent
308d9c9d6d
commit
38b12607c2
2 files changed
+127
No files matched your search
@@ -5,6 +5,7 @@ import {
|
||||
buildCreateBody,
|
||||
commitActiveDraft,
|
||||
emptyWizardPayload,
|
||||
firstInvalidIndex,
|
||||
hydrateWizardPayload,
|
||||
landingStepForEdit,
|
||||
type MedicationPayload,
|
||||
@@ -193,6 +194,102 @@ describe("validateStep", () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* v1.8.6 W4b — `firstInvalidIndex` is the multi-step forward lookahead
|
||||
* that powers the dot stepper's jump gate (`goToStep`) and its
|
||||
* `reachableUntil` ceiling. The dialog's rule:
|
||||
* - backward jumps are always allowed;
|
||||
* - a forward jump to path-index `j` is allowed iff every gate from
|
||||
* the active slot up to (but not including) `j` validates, which is
|
||||
* exactly `firstInvalidIndex(payload, stepList, from) >= j`.
|
||||
*/
|
||||
describe("firstInvalidIndex — forward-jump lookahead", () => {
|
||||
/** A daily-path payload with every gate satisfied. */
|
||||
function completeDaily(): WizardPayload {
|
||||
return {
|
||||
...withCadence("daily"),
|
||||
startsOn: new Date(Date.UTC(2026, 5, 1)),
|
||||
endsOn: null,
|
||||
timesOfDay: ["08:00"],
|
||||
};
|
||||
}
|
||||
|
||||
it("returns stepList.length when every slot from the index validates", () => {
|
||||
const payload = completeDaily();
|
||||
const list = progressIndices(payload.mode, payload.cadence.kind);
|
||||
expect(firstInvalidIndex(payload, list, 0)).toBe(list.length);
|
||||
});
|
||||
|
||||
it("stops at the first failing slot — blank name gates the path at 0", () => {
|
||||
const payload = { ...completeDaily(), name: " " };
|
||||
const list = progressIndices(payload.mode, payload.cadence.kind);
|
||||
// Step 1 (index 0) fails its gate, so no forward jump is allowed.
|
||||
expect(firstInvalidIndex(payload, list, 0)).toBe(0);
|
||||
});
|
||||
|
||||
it("a missing dose gates the path at Step 3 (path-index 2)", () => {
|
||||
const payload = { ...completeDaily(), doseAmount: "" };
|
||||
const list = progressIndices(payload.mode, payload.cadence.kind);
|
||||
// [1,2,3,...] — Step 3 sits at index 2; it's the first to fail.
|
||||
expect(firstInvalidIndex(payload, list, 0)).toBe(2);
|
||||
});
|
||||
|
||||
it("blocks a forward jump past an invalid intervening step", () => {
|
||||
// Missing times (Step 7) — daily path is [1,2,3,4,5,7,8]; Step 7
|
||||
// sits at index 5. A jump from Step 1 (index 0) to the review slot
|
||||
// (Step 8, index 6) must be refused because index 5 fails.
|
||||
const payload = { ...completeDaily(), timesOfDay: [] };
|
||||
const list = progressIndices(payload.mode, payload.cadence.kind);
|
||||
const firstInvalid = firstInvalidIndex(payload, list, 0);
|
||||
expect(firstInvalid).toBe(5);
|
||||
const targetIndex = list.indexOf(8);
|
||||
// Gate rule: forward jump allowed iff firstInvalid >= targetIndex.
|
||||
expect(firstInvalid >= targetIndex).toBe(false);
|
||||
});
|
||||
|
||||
it("allows a forward jump when all intervening gates pass", () => {
|
||||
const payload = completeDaily();
|
||||
const list = progressIndices(payload.mode, payload.cadence.kind);
|
||||
const fromIndex = 0;
|
||||
const targetIndex = list.indexOf(8);
|
||||
expect(firstInvalidIndex(payload, list, fromIndex) >= targetIndex).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("a fully-hydrated edit payload makes jump-to-last (review) reachable", () => {
|
||||
// Edit hydration populates every field, so on the recurring path
|
||||
// every gate validates and the whole path is reachable — including
|
||||
// the final review slot, so jump-to-last is one tap.
|
||||
const initial: MedicationPayload = {
|
||||
id: "med-1",
|
||||
name: "Ramipril",
|
||||
dose: "5 mg",
|
||||
category: "BLOOD_PRESSURE",
|
||||
treatmentClass: "GENERIC",
|
||||
deliveryForm: "ORAL",
|
||||
notificationsEnabled: true,
|
||||
startsOn: new Date(Date.UTC(2026, 5, 1)),
|
||||
endsOn: null,
|
||||
oneShot: false,
|
||||
schedules: [
|
||||
{
|
||||
windowStart: "06:00",
|
||||
windowEnd: "22:00",
|
||||
timesOfDay: ["08:00"],
|
||||
rrule: "FREQ=DAILY",
|
||||
rollingIntervalDays: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
const payload = hydrateWizardPayload(initial);
|
||||
const list = progressIndices(payload.mode, payload.cadence.kind);
|
||||
expect(firstInvalidIndex(payload, list, 0)).toBe(list.length);
|
||||
// Whole-path validity → jump-to-last enabled.
|
||||
expect(firstInvalidIndex(payload, list, 0) >= list.length).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("WIZARD_TREATMENT_MAPPING — Step 2 row → request body", () => {
|
||||
it("maps Blutdruck → (GENERIC, BLOOD_PRESSURE)", () => {
|
||||
expect(WIZARD_TREATMENT_MAPPING.bloodPressure).toEqual({
|
||||
|
||||
@@ -465,6 +465,36 @@ export function validateStep(payload: WizardPayload, step: number): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// firstInvalidIndex — multi-step forward lookahead
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Walks `stepList` from `fromIndex` (inclusive) and returns the index of
|
||||
* the first slot whose `validateStep` gate fails, or `stepList.length`
|
||||
* when every slot from `fromIndex` onward validates.
|
||||
*
|
||||
* This generalises the single-step `canContinue` gate
|
||||
* (`validateStep(payload, step)`) into the forward-jump lookahead the
|
||||
* dot stepper uses: a forward jump to path-index `j` is allowed iff
|
||||
* `firstInvalidIndex(payload, stepList, i) >= j` — i.e. every gate
|
||||
* between the current slot `i` and the target `j` passes. The dot
|
||||
* stepper reuses the same value as its `reachableUntil` ceiling so the
|
||||
* gate and the visual reachability never drift.
|
||||
*
|
||||
* Pure: reuses `validateStep` verbatim, no new validation logic.
|
||||
*/
|
||||
export function firstInvalidIndex(
|
||||
payload: WizardPayload,
|
||||
stepList: readonly number[],
|
||||
fromIndex: number,
|
||||
): number {
|
||||
for (let k = Math.max(0, fromIndex); k < stepList.length; k++) {
|
||||
if (!validateStep(payload, stepList[k])) return k;
|
||||
}
|
||||
return stepList.length;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// buildCreateBody — POST /api/medications request body
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in new issue
Block a user