mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
fix(onboarding): an empty multi-select is not an answer
Q2, Q4 and Q6 let "Weiter" write an answer with nothing ticked. On Q2 that answered "no areas", which is what switched every optional module off for somebody whose click meant "no preference"; on Q6 it marked the step done while recording no unit at all. An empty selection now yields no body, so the primary button is dead until something is chosen. Skip stays the way past, and Skip is the conservative derivation the completion route already applies.
This commit is contained in:
1 parent
0e79b195b0
commit
77a09a5080
4 files changed
+134
-20
No files matched your search
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* v1.39 (Wave C, C3) — Q6 cannot be answered with nothing chosen (M-k).
|
||||
*
|
||||
* "Weiter" used to be live on an untouched units screen: it wrote
|
||||
* `{ units: {} }`, which marked the step DONE without recording a preference,
|
||||
* so the account carried "asked and answered" for a question nobody answered.
|
||||
* Skip is the way past a question with no opinion, and Skip says so on the
|
||||
* ledger.
|
||||
*
|
||||
* Rendered server-side, this repo's component-test convention.
|
||||
*/
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import {
|
||||
defaultOnboardingSteps,
|
||||
emptyOnboardingNeeds,
|
||||
type OnboardingStateDto,
|
||||
} from "@/lib/onboarding/needs";
|
||||
|
||||
import { UnitsScreen } from "../units-screen";
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
useAuth: () => ({ user: null }),
|
||||
}));
|
||||
|
||||
function state(
|
||||
needs: Partial<OnboardingStateDto["needs"]>,
|
||||
): OnboardingStateDto {
|
||||
return {
|
||||
steps: defaultOnboardingSteps(),
|
||||
needs: { ...emptyOnboardingNeeds(), recordTarget: "me", ...needs },
|
||||
completedAt: null,
|
||||
firstResult: null,
|
||||
};
|
||||
}
|
||||
|
||||
function render(needs: Partial<OnboardingStateDto["needs"]>): string {
|
||||
return renderToStaticMarkup(
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<I18nProvider initialLocale="en">
|
||||
<UnitsScreen state={state(needs)} />
|
||||
</I18nProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the primary button's own tag disabled? Read off the tag rather than the
|
||||
* whole page, and off the attribute rather than the string "disabled", which
|
||||
* also occurs in the button's `disabled:` utility classes.
|
||||
*/
|
||||
function nextIsDisabled(html: string): boolean {
|
||||
const at = html.indexOf('data-slot="onboarding-next"');
|
||||
expect(at).toBeGreaterThan(-1);
|
||||
const open = html.lastIndexOf("<button", at);
|
||||
return html.slice(open, html.indexOf(">", at) + 1).includes('disabled=""');
|
||||
}
|
||||
|
||||
describe("<UnitsScreen>", () => {
|
||||
it("does not let the person continue without choosing a unit", () => {
|
||||
const html = render({ areas: ["glucose"] });
|
||||
expect(nextIsDisabled(html)).toBe(true);
|
||||
// Skip is still there: it is the honest way past a question with no
|
||||
// opinion, and it says "skipped" on the ledger.
|
||||
expect(html).toContain('data-slot="onboarding-skip"');
|
||||
});
|
||||
|
||||
it("lets the person continue once a unit is chosen", () => {
|
||||
const html = render({
|
||||
areas: ["glucose"],
|
||||
units: { glucoseUnit: "mg/dL", unitPreference: null },
|
||||
});
|
||||
expect(nextIsDisabled(html)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -54,9 +54,15 @@ export function UnitsScreen({ state }: { state: OnboardingStateDto }) {
|
||||
|
||||
const back = previousScreen(state, "units");
|
||||
|
||||
const answerBody = unitsAnswerBody({ asked, glucoseUnit, unitPreference });
|
||||
|
||||
async function persist(
|
||||
input:
|
||||
ReturnType<typeof unitsAnswerBody> | { step: "units"; status: "skipped" },
|
||||
| NonNullable<ReturnType<typeof unitsAnswerBody>>
|
||||
| {
|
||||
step: "units";
|
||||
status: "skipped";
|
||||
},
|
||||
) {
|
||||
try {
|
||||
const written = await answer.mutateAsync(input);
|
||||
@@ -121,9 +127,10 @@ export function UnitsScreen({ state }: { state: OnboardingStateDto }) {
|
||||
<StepActions
|
||||
backHref={back ? screenHref(back) : undefined}
|
||||
onSkip={() => void persist({ step: "units", status: "skipped" })}
|
||||
onNext={() =>
|
||||
void persist(unitsAnswerBody({ asked, glucoseUnit, unitPreference }))
|
||||
}
|
||||
onNext={() => {
|
||||
if (answerBody) void persist(answerBody);
|
||||
}}
|
||||
nextDisabled={answerBody === null}
|
||||
pending={answer.isPending}
|
||||
/>
|
||||
</section>
|
||||
|
||||
Reference in new issue
Block a user