mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
fix(onboarding): a connected source is acknowledged, not offered
`chooseFirstResultTask` read the answers alone, so an account whose WHOOP had been feeding the dashboard for months was sent to go and connect WHOOP. It takes the delivering sources now; the server still picks from the answers, having no status in hand, and the screen re-picks once the envelope resolves. The fall-through stops where there is nowhere to fall to: a person whose one answer is an already-connected source keeps the step and is told what was found, rather than ending the flow on no result at all.
This commit is contained in:
1 parent
da58d16642
commit
8d81ac0708
6 files changed
+194
-9
No files matched your search
@@ -19,6 +19,7 @@
|
||||
*/
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import type { SyncVerdict } from "@/lib/integrations/sync-verdict";
|
||||
@@ -101,25 +102,33 @@ function statuses(overrides: StatusOverrides | null) {
|
||||
};
|
||||
}
|
||||
|
||||
function state(): OnboardingStateDto {
|
||||
function state(
|
||||
needs: Partial<OnboardingStateDto["needs"]> = {},
|
||||
): OnboardingStateDto {
|
||||
return {
|
||||
steps: defaultOnboardingSteps(),
|
||||
needs: {
|
||||
...emptyOnboardingNeeds(),
|
||||
recordTarget: "me",
|
||||
sources: ["whoop"],
|
||||
...needs,
|
||||
},
|
||||
completedAt: null,
|
||||
firstResult: null,
|
||||
};
|
||||
}
|
||||
|
||||
function render(overrides: StatusOverrides | null) {
|
||||
function render(
|
||||
overrides: StatusOverrides | null,
|
||||
needs: Partial<OnboardingStateDto["needs"]> = {},
|
||||
) {
|
||||
statuses(overrides);
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
<FirstResultScreen state={state()} />
|
||||
</I18nProvider>,
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<I18nProvider initialLocale="en">
|
||||
<FirstResultScreen state={state(needs)} />
|
||||
</I18nProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -191,4 +200,44 @@ describe("<FirstResultScreen> — the connect arm", () => {
|
||||
expect(html).not.toContain("data-connect-slot=");
|
||||
expect(html).toContain('data-slot="onboarding-task-checking"');
|
||||
});
|
||||
|
||||
/**
|
||||
* v1.38.19 (wave B / I1) — the heading is chosen from the answers alone on
|
||||
* the server, because no status is in hand there. Once the envelope has
|
||||
* resolved the screen re-picks: a source that is already delivering is not
|
||||
* a task, and "Connect WHOOP" over a WHOOP that has been feeding the
|
||||
* dashboard for months is the flow talking past the account it can see.
|
||||
*/
|
||||
it("stops offering a source that is already delivering", () => {
|
||||
const html = render(
|
||||
{
|
||||
connected: true,
|
||||
verdict: "fresh",
|
||||
lastSuccessAt: "2026-09-10T06:35:00.000Z",
|
||||
},
|
||||
{ medication: "yes" },
|
||||
);
|
||||
expect(html).toContain('data-task="add-medication"');
|
||||
expect(html).not.toContain("Connect WHOOP");
|
||||
});
|
||||
|
||||
it("acknowledges the connection when there is nothing to fall through to", () => {
|
||||
// The whole answer is one already-connected source. Dropping the step
|
||||
// would end the flow on nothing at all, so the screen keeps it and says
|
||||
// what it found — and that is the result it records.
|
||||
const html = render({
|
||||
connected: true,
|
||||
verdict: "fresh",
|
||||
lastSuccessAt: "2026-09-10T06:35:00.000Z",
|
||||
});
|
||||
expect(html).toContain('data-task="connect-source"');
|
||||
expect(html).toContain('data-connect-state="fresh"');
|
||||
expect(html).toContain("WHOOP is already connected");
|
||||
});
|
||||
|
||||
it("keeps the answer's heading while the envelope is in flight", () => {
|
||||
const html = render(null, { medication: "yes" });
|
||||
expect(html).toContain('data-task="connect-source"');
|
||||
expect(html).toContain("Connect WHOOP");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
AREA_PAGE_HREF,
|
||||
AREA_READING_TARGETS,
|
||||
connectSourceView,
|
||||
deliveringSources,
|
||||
SOURCE_INTEGRATION,
|
||||
type ConnectSourceSlot,
|
||||
} from "@/lib/onboarding/first-result-config";
|
||||
@@ -70,7 +71,23 @@ export function FirstResultScreen({ state }: { state: OnboardingStateDto }) {
|
||||
const { t } = useTranslations();
|
||||
const router = useRouter();
|
||||
const answer = useOnboardingAnswer();
|
||||
const offer = chooseFirstResultTask(state.needs);
|
||||
// The server picked the order from the answers alone — it had no status in
|
||||
// hand. Re-pick here once the envelope resolves: a source that is already
|
||||
// delivering is not a task, and offering to connect it is the flow talking
|
||||
// past an account it can see. The same query key the task body reads, so
|
||||
// this costs no second request.
|
||||
const statuses = useIntegrationStatuses(true);
|
||||
const delivering = useMemo(
|
||||
() => deliveringSources(statuses.data?.integrations),
|
||||
[statuses.data],
|
||||
);
|
||||
// …but only where there is somewhere to fall to. A person whose one answer
|
||||
// is a source that is already delivering still gets an end to the flow: the
|
||||
// screen acknowledges the connection it found rather than dropping the step
|
||||
// and ending on nothing.
|
||||
const offer =
|
||||
chooseFirstResultTask(state.needs, delivering) ??
|
||||
chooseFirstResultTask(state.needs);
|
||||
const back = previousScreen(state, "first-result");
|
||||
|
||||
// Completed only when the LEDGER says so as well: a restart puts the step
|
||||
|
||||
Reference in new issue
Block a user