fix(onboarding): checklist queries follow its own setup rule

The card's visibility rule gained a "ran the setup flow" disjunct; the
component's query gate kept restating the older half of it. A wizard-run
account past five readings therefore saw a permanent checklist whose
medication, data-source, notification and insight rows were frozen at
"not done", because every supporting query was disabled.

Both places now read one exported predicate, isStillInSetup, which also
keeps the rows live for a restarted run nobody finished.
This commit is contained in:
Marc-André Bombeck committed 2026-09-11 09:07:41 +02:00
1 parent 274923a060
commit dec5732687
4 files changed
+265 -18

No files matched your search

@@ -9,6 +9,7 @@ import {
CHECKLIST_BASE_ITEM_IDS,
CHECKLIST_ITEM_IDS,
isProfileComplete,
isStillInSetup,
shouldShowChecklist,
trendHintFor,
upcomingVisitCountFrom,
@@ -608,3 +609,75 @@ describe("shouldShowChecklist and the setup flow", () => {
).toBe(false);
});
});
describe("isStillInSetup", () => {
// The one predicate both the visibility rule and the component's query gate
// read, so the card can never render with its supporting queries switched
// off (research I9).
it("is true for a record that ran the flow, whatever its reading count", () => {
expect(
isStillInSetup({
onboarding: onboardingState(),
onboardingCompletedAt: "2026-01-01T00:00:00Z",
measurementCount: 120,
}),
).toBe(true);
});
it("is true for a restarted run nobody finished (M-p)", () => {
const restarted = onboardingState({ completedAt: null });
expect(
isStillInSetup({
onboarding: restarted,
// `restart` deliberately leaves the account stamp alone.
onboardingCompletedAt: "2026-01-01T00:00:00Z",
measurementCount: 120,
}),
).toBe(true);
});
it("keeps the pre-flow rule for a record that never entered the flow", () => {
const untouched: OnboardingStateDto = {
steps: defaultOnboardingSteps(),
needs: emptyOnboardingNeeds(),
completedAt: null,
firstResult: null,
};
expect(
isStillInSetup({
onboarding: untouched,
onboardingCompletedAt: "2026-01-01T00:00:00Z",
measurementCount: 10,
}),
).toBe(false);
expect(
isStillInSetup({
onboarding: untouched,
onboardingCompletedAt: "2026-01-01T00:00:00Z",
measurementCount: 4,
}),
).toBe(true);
expect(
isStillInSetup({
onboarding: null,
onboardingCompletedAt: null,
measurementCount: 10,
}),
).toBe(true);
});
it("is the rule `shouldShowChecklist` itself applies", () => {
const args = {
onboarding: onboardingState(),
onboardingCompletedAt: "2026-01-01T00:00:00Z",
measurementCount: 120,
};
expect(
shouldShowChecklist({
...args,
dismissedAll: false,
items: buildChecklist(inputs({ measurementCount: 120 })),
}),
).toBe(isStillInSetup(args));
});
});
+35 -11
View File
@@ -282,11 +282,7 @@ export function checklistProgress(items: ChecklistItem[]): ChecklistProgress {
* unfinished, because the one task the flow offered is not one of the six
* rows: "every row done" and "the setup finished" are different statements.
*/
export function shouldShowChecklist(args: {
onboardingCompletedAt: string | null;
measurementCount: number;
dismissedAll: boolean;
items: ChecklistItem[];
export interface StillInSetupInputs {
/**
* v1.39 (C1) — the needs-based setup state for this record, or null before
* the account payload resolves. A record that never entered the flow reads
@@ -295,17 +291,45 @@ export function shouldShowChecklist(args: {
* on the rule it already had.
*/
onboarding?: OnboardingStateDto | null;
}): boolean {
onboardingCompletedAt: string | null;
measurementCount: number;
}
/**
* v1.39 (Wave C, C1) — "is this record still being set up?", as ONE predicate.
*
* Two places decide this: whether the card renders at all, and whether the
* component fetches the data its rows are about. They were two restatements of
* the same rule and they drifted — the visibility rule gained
* `hasEnteredOnboardingFlow`, the query gate kept the older half, and a
* wizard-run account past five readings got a permanently visible card whose
* every network-backed row read "not done" (research I9). A record whose
* restarted run was abandoned is in the same state (M-p): `restart` clears the
* record's own `completedAt` and deliberately not the account stamp, so
* `hasEnteredOnboardingFlow` is what keeps its rows live.
*
* Exported so the component calls it rather than restating it.
*/
export function isStillInSetup(args: StillInSetupInputs): boolean {
return (
hasEnteredOnboardingFlow(args.onboarding ?? null) ||
args.onboardingCompletedAt == null ||
args.measurementCount < 5
);
}
export function shouldShowChecklist(
args: StillInSetupInputs & {
dismissedAll: boolean;
items: ChecklistItem[];
},
): boolean {
if (args.dismissedAll) return false;
const onboarding = args.onboarding ?? null;
const visible = visibleChecklist(args.items);
if (visible.length === 0) return false;
const stillInSetup =
hasEnteredOnboardingFlow(onboarding) ||
args.onboardingCompletedAt == null ||
args.measurementCount < 5;
if (!stillInSetup) return false;
if (!isStillInSetup(args)) return false;
if (!isOnboardingSettled(onboarding)) return true;
return visible.some((item) => !item.done);